ReachGraph Module Architecture

Audience: ReachGraph service owners and consumers (Policy Engine, Console, CLI, Export Center) that query “why reachable?” evidence. Source of truth: src/ReachGraph/ and src/__Libraries/StellaOps.ReachGraph* / StellaOps.Reachability.Core. Where a doc claim and the code disagree, the code wins.

Overview

The ReachGraph module provides a unified store for reachability subgraphs, giving Stella Ops fast, deterministic, audit-ready answers to “exactly why a dependency is reachable.” It consolidates data from Scanner, Signals, and Attestor into content-addressed artifacts with edge-level explainability.

Problem Statement

Before ReachGraph, reachability data was scattered across multiple modules:

ModuleDataLimitation
Scanner.CallGraphCallGraphSnapshotNo unified query API
SignalsReachabilityFactDocumentRuntime-focused, not auditable
AttestorPoE JSONPer-CVE only, no slice queries
GraphGeneric nodes/edgesNot optimized for “why reachable?”

Result: Answering “why is lodash reachable?” required querying multiple systems with no guarantee of consistency or auditability.

Solution

ReachGraph provides:

  1. Unified Schema: Extends PoE subgraph format with edge explainability
  2. Content-Addressed Store: All artifacts identified by BLAKE3 digest
  3. Slice Query API: Fast queries by package, CVE, entrypoint, or file
  4. Deterministic Replay: Verify that same inputs produce same graph
  5. DSSE Signing: Offline-verifiable proofs

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                       Consumers                                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐        │
│  │  Policy  │  │   Web    │  │   CLI    │  │ Export   │        │
│  │  Engine  │  │ Console  │  │          │  │ Center   │        │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘        │
└───────┼─────────────┼─────────────┼─────────────┼───────────────┘
        │             │             │             │
        └─────────────┴──────┬──────┴─────────────┘
                             │
┌────────────────────────────▼────────────────────────────────────┐
│                    ReachGraph WebService                         │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  REST API (MVC controllers)                               │   │
│  │  ReachGraphController     /v1/reachgraphs/*  (store)      │   │
│  │  ReachabilityController   /v1/reachability/* (unified)    │   │
│  │  CveMappingController     /v1/cve-mappings/* (CVE↔symbol) │   │
│  └──────────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Slice Query Engine                                       │   │
│  │  - Package slice (by PURL)                                │   │
│  │  - CVE slice (paths to vulnerable sinks)                  │   │
│  │  - Entrypoint slice (reachable from entry)                │   │
│  │  - File slice (changed file impact)                       │   │
│  └──────────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Replay Service (ReachGraphReplayService)                 │   │
│  │  - Verify input digests vs stored provenance              │   │
│  │  - Recompute stored graph digest (see note: not yet a     │   │
│  │    full rebuild-from-inputs) and compare                  │   │
│  │  - Record every attempt in reachgraph.replay_log          │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────┬───────────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────────┐
│                    ReachGraph Core Library                       │
│  ┌────────────────┐  ┌────────────────┐  ┌────────────────┐    │
│  │     Schema     │  │ Serialization  │  │    Signing     │    │
│  │                │  │                │  │                │    │
│  │ ReachGraphMin  │  │ Canonical JSON │  │ DSSE Wrapper   │    │
│  │ EdgeExplanation│  │ BLAKE3 Digest  │  │ Attestor Int.  │    │
│  │ Provenance     │  │ Compression    │  │                │    │
│  └────────────────┘  └────────────────┘  └────────────────┘    │
└─────────────────────────────┬───────────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────────┐
│                    Persistence Layer                             │
│  ┌────────────────────────┐  ┌────────────────────────┐        │
│  │      PostgreSQL        │  │        Valkey          │        │
│  │   schema: reachgraph   │  │  Full graph (24h TTL)  │        │
│  │  reachgraph.subgraphs  │  │  Hot slice cache       │        │
│  │  reachgraph.slice_cache│  │  (30min TTL)           │        │
│  │  reachgraph.replay_log │  │  tenant-scoped keys    │        │
│  └────────────────────────┘  └────────────────────────┘        │
└─────────────────────────────────────────────────────────────────┘
                              ▲
                              │
┌─────────────────────────────┴───────────────────────────────────┐
│                       Producers                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │   Scanner    │  │   Signals    │  │   Attestor   │          │
│  │  CallGraph   │  │ RuntimeFacts │  │     PoE      │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
└─────────────────────────────────────────────────────────────────┘

Data Model

ReachGraphMinimal (v1)

The core schema extends the PoE predicate format:

{
  "schemaVersion": "reachgraph.min@v1",
  "artifact": {
    "name": "svc.payments",
    "digest": "sha256:abc123...",
    "env": ["linux/amd64"]
  },
  "scope": {
    "entrypoints": ["/app/bin/svc"],
    "selectors": ["prod"],
    "cves": ["CVE-2024-1234"]
  },
  "nodes": [...],
  "edges": [...],
  "provenance": {...},
  "signatures": [...]
}

The graph’s own content-addressed identity (digest) is a BLAKE3-256 hash in the form blake3:{64-hex} (see Content Addressing). The artifact.digest field above is the subject image/artifact digest and uses the sha256:/sha512: convention (validated by ReachGraphStoreService.ValidateGraph). Do not confuse the two: the row digest primary key is blake3:…, the artifact_digest column is sha256:…/sha512:….

Source: src/__Libraries/StellaOps.ReachGraph/Schema/ReachGraphMinimal.cs (ReachGraphMinimal, ReachGraphArtifact, ReachGraphScope, ReachGraphSignature), ReachGraphProvenance.cs (ReachGraphProvenance/ReachGraphInputs/ReachGraphAnalyzer).

Edge Explainability

Every edge (ReachGraphEdge: from, to, why) carries an EdgeExplanation (type, optional loc, optional guard, required confidence in [0.0, 1.0], optional metadata) explaining why it exists. The EdgeExplanationType enum (serialized as a JSON string) has eleven members:

TypeDescriptionExample Guard
ImportStatic import (ES6 import, Python import, using)-
DynamicLoadRuntime load (require(), dlopen, LoadLibrary)-
ReflectionReflective call (Class.forName, Type.GetType)-
FfiForeign function interface (JNI, P/Invoke, ctypes)-
EnvGuardEnv variable check (process.env.X, os.environ.get)DEBUG=true
FeatureFlagFeature flag (LaunchDarkly, unleash, custom)FEATURE_X=enabled
PlatformArchPlatform/arch guard (process.platform, runtime.GOOS)os=linux
TaintGateTaint gate (sanitization, validation)-
LoaderRuleLoader rule (PLT/IAT/GOT entry)RTLD_LAZY
DirectCallDirect call (static, virtual, delegate)-
UnknownCannot determine explanation type-

Source: src/__Libraries/StellaOps.ReachGraph/Schema/EdgeExplanation.cs, ReachGraphEdge.cs, ReachGraphNode.cs (node kinds: Package, File, Function, Symbol, Class, Module).

Content Addressing

All graphs are identified by a BLAKE3-256 digest, emitted in the form blake3:{64-hex-lowercase} (ReachGraphDigestComputer.ComputeDigest):

Source: src/__Libraries/StellaOps.ReachGraph/Hashing/ReachGraphDigestComputer.cs (uses the Blake3 package), Serialization/CanonicalReachGraphSerializer*.cs.

Persistence Schema

The durable store is the reachgraph PostgreSQL schema, created and migrated by the embedded SQL migration 001_reachgraph_store.sql in StellaOps.ReachGraph.Persistence. Migrations run on startup via AddStartupMigrations (module name ReachGraph.Persistence, schema reachgraph); they are skipped in the Testing environment and can be disabled with ReachGraph:StartupMigrations:Enabled=false.

TablePurposeKey columns
reachgraph.subgraphsContent-addressed graph blobsdigest (PK, blake3:…), artifact_digest (sha256:/sha512:), tenant_id, scope JSONB, node_count, edge_count, blob BYTEA (gzip), blob_size_bytes, provenance JSONB, created_at
reachgraph.slice_cachePrecomputed slices (DB-side)cache_key (PK), subgraph_digest (FK → subgraphs, ON DELETE CASCADE), slice_blob, query_type (package/cve/entrypoint/file), query_params JSONB, expires_at, hit_count
reachgraph.replay_logAudit log of replay attemptsid (UUID PK), subgraph_digest, input_digests JSONB, computed_digest, matches, divergence JSONB, tenant_id, duration_ms, computed_at

Indexes back the common access paths: composite (tenant_id, artifact_digest, created_at DESC), artifact-only lookup, GIN indexes on scope->'cves', scope->'entrypoints', and provenance->'inputs', plus replay-log indexes by digest, by tenant, and a partial index on matches = false.

Row-Level Security. All three tables have RLS enabled with USING (tenant_id = current_setting('app.tenant_id', true)) policies (the slice cache inherits its tenant through the subgraph FK). Because current_setting(..., true) returns NULL when unset, an unset tenant blocks all rows.

Note: reachgraph.slice_cache (Postgres-side) and the Valkey slice cache are two distinct caches — the runtime slice path (ReachGraphSliceService) currently uses the Valkey IReachGraphCache; the slice_cache table is defined and migrated but is not the live read path for slices.

Source: src/__Libraries/StellaOps.ReachGraph.Persistence/Migrations/001_reachgraph_store.sql, Postgres/ReachGraphDataSource.cs, PostgresReachGraphRepository.*.cs, Program.cs (AddStartupMigrations).

API Design

Core Endpoints

The store surface is the ReachGraphController ([Route("v1/reachgraphs")]). Reads use the reachgraph-read rate-limiter policy (100 req/min/tenant); writes use reachgraph-write (20 req/min/tenant).

MethodPathDescription
POST/v1/reachgraphsUpsert subgraph (idempotent by digest; 201 on create, 200 if already stored)
GET/v1/reachgraphs/{digest}Get full subgraph (sets ETag, Cache-Control 24h)
GET/v1/reachgraphs/{digest}/sliceQuery slice (one of q/cve/entrypoint/file required)
GET/v1/reachgraphs/by-artifact/{artifactDigest}List subgraph summaries for an artifact (limit, default 50)
POST/v1/reachgraphs/replayVerify determinism
DELETE/v1/reachgraphs/{digest}Delete a subgraph; requires an authenticated StellaOps principal with graph:admin

Authentication & tenancy. Outside the Testing environment the service registers Authority resource-server auth (AddStellaOpsResourceServerAuthentication, config section Authority:ResourceServer) and a fallback authorization policy that requires an authenticated user on every endpoint not marked [AllowAnonymous] (only /healthz, /readyz, the OpenAPI document, and the build-info endpoint are anonymous). Tenant resolution is claim-first and header-refusing: the validated stellaops:tenant claim wins (with tenant_id/tenant/tid aliases), via StellaOpsTenantResolver; the canonical X-StellaOps-TenantId header is honoured only for stellaops:admin principals, and the legacy X-Tenant-ID header is never a tenancy source. A request with no resolvable tenant claim is rejected (400 tenant_missing in the controller, InvalidOperationException on the evidence-cache path) — it never defaults. (Program.cs:270-288 ResolveTenantId; ReachGraphController.cs:301-322 TryResolveTenant; StellaOps.Auth.ServerIntegration/Tenancy/StellaOpsTenantResolver.cs.) Note: aside from the graph:admin gate on DELETE, individual store/slice/replay endpoints do not require a specific read/write scope today — they rely on the fallback “authenticated user” policy plus tenant scoping. (graph:read/graph:write scopes exist in StellaOpsScopes.cs but are not enforced per-route in this service.)

Source: ReachGraphController.cs, Program.cs (auth/rate-limit/health registration), Security/ReachGraphDestructiveActionAuthorizer.cs.

Slice Query Types

  1. Package Slice (?q=pkg:npm/lodash@4.17.21)

    • Returns subgraph containing package and neighbors
    • Configurable depth and direction
  2. CVE Slice (?cve=CVE-2024-1234)

    • Returns all paths from entrypoints to vulnerable sinks
    • Includes edge explanations for each hop
  3. Entrypoint Slice (?entrypoint=/app/bin/svc)

    • Returns everything reachable from entry
    • Optionally filtered to paths reaching sinks
  4. File Slice (?file=src/**/*.ts)

    • Returns impact of changed files
    • Useful for PR-based analysis

Integration Points

Upstream (Data Producers)

Relationship to the generic Graph module. ReachGraph is a distinct store specialized for “why reachable?” queries; it does not consume node/edge events from the generic Graph module today. The Graph row in the Problem Statement table is a capability comparison, not a live event-ingestion integration. ReachGraph is populated by HTTP upsert (POST /v1/reachgraphs) from Scanner.Worker, not by a Graph event bus.

Downstream (Data Consumers)

Determinism Guarantees

  1. Canonical Serialization

    • Sorted object keys (lexicographic)
    • Sorted arrays by deterministic field
    • UTC ISO-8601 timestamps
    • No null fields (omit when null)
  2. Replay Verification

    • POST /v1/reachgraphs/replay loads the stored graph for expectedDigest, verifies the supplied input digests against the stored provenance (inputsVerified), and recomputes the digest of the stored graph
    • Returns {match, computedDigest, expectedDigest, durationMs, inputsVerified}; match is true when the recomputed digest equals expectedDigest
    • Records the attempt in reachgraph.replay_log for the audit trail when the expected graph is found — including a digest mismatch on a found graph (Matches=false). Two branches deliberately do not write a row: the graph-not-found case (nothing to replay) and the catch fallback (the response carries computedDigest="ERROR"). Failed-lookup and errored replays are therefore an intentional audit gap, not a guaranteed record (ReachGraphReplayService.RecordReplayAsync, success path only).
    • Current limitation: the service recomputes the digest of the stored graph rather than rebuilding the graph from the supplied SBOM/VEX/callgraph/ runtime-facts inputs. A full rebuild-from-inputs replay is a roadmap item (see the explicit comment in ReachGraphReplayService.ReplayAsync).

    Source: src/ReachGraph/StellaOps.ReachGraph.WebService/Services/ReachGraphReplayService.cs.

  3. Content Addressing

    • Same content always produces same digest
    • Enables cache keying and deduplication

Performance Characteristics

These are design targets (not measured SLOs enforced in code):

OperationTarget LatencyNotes
Slice queryP95 < 200msCached in Valkey (30 min slice TTL)
Full graph retrievalP95 < 500msgzip blob; full-graph cache TTL 24h
UpsertP95 < 1sIdempotent, gzip compression
ReplayP95 < 5sCurrently bounded by stored-graph re-hash, not full rebuild

Security Considerations

  1. Tenant Isolation: RLS policies enforce at database level
  2. Rate Limiting: 100 req/min reads, 20 req/min writes
  3. DSSE Signing: The schema reserves a signatures array and the Core library provides DSSE wrap/verify hooks so graphs can be made offline-verifiable. The running WebService does not currently sign graphs or register a signer (see Integration Points → Attestor); when signatures are present and a signer is wired, upsert verifies them and rejects invalid signatures.
  4. Input Validation: ReachGraphStoreService.ValidateGraph requires SchemaVersion, Artifact.Name, Artifact.Digest (must be sha256:/sha512:), and Provenance.Inputs.Sbom; it warns (does not reject) when the provenance timestamp is older than 24 hours. The unified-query and CVE-mapping controllers validate required body fields and return 400 otherwise.
  5. Cache Tenant Authority: Graph and slice caches are acceleration only. The durable repository must confirm the request tenant owns the graph before cached graph or slice evidence can be returned. Live cache keys are tenant-scoped from the current request tenant, not a process-wide default.
  6. Startup Migrations: The WebService registers ReachGraphDataSource and runs embedded StellaOps.ReachGraph.Persistence SQL migrations for the reachgraph schema on startup. Fresh PostgreSQL databases must converge without manual psql; test harnesses may disable the migration host explicitly while replacing storage with in-memory doubles.
  7. Destructive Authorization: Graph deletion is fail-closed. DELETE /v1/reachgraphs/{digest} requires an authenticated StellaOps principal carrying graph:admin; tenant headers, graph write scope, or anonymous direct service access are insufficient and must leave the stored graph intact.

Unified Query Interface

The ReachGraph module exposes a Unified Reachability Query API that provides a single facade for static, runtime, and hybrid queries.

API Endpoints

EndpointMethodDescription
/v1/reachability/staticPOSTQuery static reachability from call graph analysis
/v1/reachability/runtimePOSTQuery runtime reachability from observed execution facts
/v1/reachability/hybridPOSTCombine static and runtime for best-effort verdict
/v1/reachability/batchPOSTBatch query for CVE vulnerability analysis

Adapters

The unified query interface is backed by two adapters:

  1. ReachGraphStoreAdapter: Implements IReachGraphAdapter from StellaOps.Reachability.Core

    • Queries static reachability from stored call graphs
    • Uses BFS from entrypoints to target symbols
    • Returns StaticReachabilityResult with distance, path, and evidence URIs
  2. SignalsHttpAdapter: Implements ISignalsAdapter from StellaOps.Reachability.Core

    • Queries runtime observation facts from the live Signals service over HTTP
    • Resolves the base URL from Signals:BaseUrl, STELLAOPS_SIGNALS_URL, or the local signals.stella-ops.local alias
    • Pulls GET /signals/facts/{subjectKey} and translates the stored reachability fact into RuntimeReachabilityResult
    • The in-memory adapter remains test-only and is no longer wired into the live host

Hybrid Query Flow

┌────────────────┐
│  Hybrid Query  │
│   Request      │
└───────┬────────┘
        │
        ▼
┌───────────────────────────────────────────┐
│         ReachabilityIndex Facade          │
│  (StellaOps.Reachability.Core)            │
└───────┬───────────────────────┬───────────┘
        │                       │
        ▼                       ▼
┌───────────────┐       ┌───────────────┐
│ ReachGraph    │       │ Signals       │
│ StoreAdapter  │       │ Adapter       │
└───────┬───────┘       └───────┬───────┘
        │                       │
        ▼                       ▼
┌───────────────┐       ┌───────────────┐
│ PostgreSQL +  │       │ Signals svc   │
│ Valkey Cache  │       │ (HTTP facts)  │
└───────────────┘       └───────────────┘

The unified reachability surface is ReachabilityController ([Route("v1/reachability")]) backed by IReachabilityIndex / ReachabilityIndex from StellaOps.Reachability.Core. The four endpoints accept a SymbolRef plus an artifactDigest; hybrid/batch also accept includeStatic, includeRuntime, observationWindow (default 7 days), and minConfidenceThreshold (default 0.8). The static adapter is ReachGraphStoreAdapter; the runtime adapter is SignalsHttpAdapter, which calls GET /signals/facts/{artifactDigest} on the live Signals service. Source: Controllers/ReachabilityController.cs, Services/ReachGraphStoreAdapter.cs, Services/SignalsHttpAdapter.cs, Program.cs.

Query Models

SymbolRef - Identifies a code symbol. purl, namespace, type, and method are required; signature defaults to empty. Use "_" for type (and namespace) when there is no enclosing class/namespace. The record also exposes computed canonicalId (SHA-256 of purl|namespace|type|method|signature) and displayName properties:

{
  "purl": "pkg:nuget/System.Net.Http@8.0.0",
  "namespace": "System.Net.Http",
  "type": "HttpClient",
  "method": "GetAsync",
  "signature": "(string)"
}

StaticReachabilityResult:

{
  "symbol": { "purl": "...", "namespace": "...", "type": "...", "method": "..." },
  "artifactDigest": "sha256:abc123...",
  "isReachable": true,
  "pathCount": 2,
  "shortestPathLength": 3,
  "entrypoints": ["/app/bin/svc"],
  "guards": [{ "type": "FeatureFlag", "key": "FEATURE_X", "value": "enabled" }],
  "evidenceUris": ["stella:evidence/reachgraph/sha256:abc123/symbol:..."],
  "analyzedAt": "2025-06-15T12:00:00Z",
  "analyzerVersion": "..."
}

RuntimeReachabilityResult:

{
  "symbol": { ... },
  "artifactDigest": "sha256:abc123...",
  "wasObserved": true,
  "observationWindow": "7.00:00:00",
  "windowStart": "2025-06-08T12:00:00Z",
  "windowEnd": "2025-06-15T12:00:00Z",
  "hitCount": 1250,
  "firstSeen": "2025-06-10T08:00:00Z",
  "lastSeen": "2025-06-15T12:00:00Z",
  "contexts": [{ "containerId": "abc", "processId": 42, "route": "/api", "environment": "production", "frequency": 0.85 }],
  "evidenceUris": ["stella:evidence/signals/sha256:abc123/symbol:..."],
  "traffic": { "requestCount": 100000, "percentile": "p95", "environments": ["production"] },
  "agentVersion": "..."
}

HybridReachabilityResult: verdict is a VerdictRecommendation object (vexStatus is one of affected/not_affected/under_investigation, with an optional justification/statusNotes/requiresManualReview), not a bare string. latticeState carries the computed 8-state lattice value, and contentDigest is a computed sha256: hash of the canonical content:

{
  "symbol": { ... },
  "artifactDigest": "sha256:abc123...",
  "latticeState": "ConfirmedReachable",
  "confidence": 0.92,
  "staticResult": { ... },
  "runtimeResult": { ... },
  "verdict": { "vexStatus": "affected", "justification": null },
  "evidence": { "uris": [...], "collectedAt": "2025-06-15T12:00:00Z" },
  "computedAt": "2025-06-15T12:00:00Z"
}

Source: src/__Libraries/StellaOps.Reachability.Core/SymbolRef.cs, StaticReachabilityResult.cs, RuntimeReachabilityResult.cs, HybridReachabilityResult.cs.


CVE-Symbol Mapping API

The WebService also hosts CveMappingController ([Route("v1/cve-mappings")]), which maps CVE identifiers to the specific vulnerable functions/methods used during reachability analysis. It is backed by ICveSymbolMappingService from StellaOps.Reachability.Core.CveMapping.

MethodPathDescription
GET/v1/cve-mappings/{cveId}All symbol mappings for a CVE (404 if none)
GET/v1/cve-mappings/by-package?purl=…Mappings affecting a package
GET/v1/cve-mappings/by-symbol?symbol=…&language=…Search mappings by symbol name
POST/v1/cve-mappingsAdd/update a CVE↔symbol mapping (reachgraph-write)
POST/v1/cve-mappings/analyze-patchExtract vulnerable symbols from a commit URL or inline diff
POST/v1/cve-mappings/{cveId}/enrichEnrich a CVE mapping from the OSV database
GET/v1/cve-mappings/statsMapping-corpus statistics (totals, by-source, by-type, avg confidence)

A VulnerableSymbol carries a CanonicalSymbol (namespace/type/method/signature, SymbolSource, optional PURL), a VulnerabilityType, a confidence score, an optional source file, and an optional LineRange. MappingSource and VulnerabilityType are parsed case-insensitively and default to Unknown.

Source: src/ReachGraph/StellaOps.ReachGraph.WebService/Controllers/CveMappingController.cs, StellaOps.Reachability.Core (CveMapping, Symbols namespaces).


Lattice Triage Service

Overview

The Lattice Triage Service provides a workflow-oriented surface on top of the 8-state reachability lattice, enabling operators to visualise lattice states, apply evidence, perform manual overrides, and maintain a full audit trail of every state transition.

Library: StellaOps.Reachability.Core Namespace: StellaOps.Reachability.Core

Models

TypePurpose
LatticeTriageEntryPer-(component, CVE) snapshot: current state, confidence, VEX status, full transition history. Content-addressed EntryId (triage:sha256:…). Computed RequiresReview / HasOverride.
LatticeTransitionRecordImmutable log entry per state change: from/to state, confidence before/after, trigger, reason, actor, evidence digests, timestamp. Computed IsManualOverride.
LatticeTransitionTriggerEnum: StaticAnalysis, RuntimeObservation, ManualOverride, SystemReset, AutomatedRule. Serialised as JsonStringEnumConverter.
LatticeOverrideRequestOperator request to force a target state with reason, actor, and evidence digests.
LatticeOverrideResultOutcome of an override: applied flag, updated entry, transition, optional warning.
LatticeTriageQueryFilters: State?, RequiresReview?, ComponentPurlPrefix?, Cve?, Limit (default 100), Offset.

Service Interface (ILatticeTriageService)

MethodDescription
GetOrCreateEntryAsync(componentPurl, cve)Returns existing entry or creates one at Unknown state.
ApplyEvidenceAsync(componentPurl, cve, evidenceType, reason?, evidenceDigests?)Delegates to ReachabilityLattice.ApplyEvidence, records transition. (No actor parameter — only OverrideStateAsync/ResetAsync capture an actor.)
OverrideStateAsync(request)Forces target state via Reset + ForceState sequence. Warns when overriding Confirmed* states.
ListAsync(query)Filters + pages entries; ordered by UpdatedAt descending.
GetHistoryAsync(purl, cve)Returns full transition log for an entry.
ResetAsync(purl, cve, actor, reason)Resets entry to Unknown, records SystemReset transition.

VEX Status Mapping

Lattice StateVEX Status
Unknown, StaticReachable, Contestedunder_investigation
StaticUnreachable, RuntimeUnobserved, ConfirmedUnreachablenot_affected
RuntimeObserved, ConfirmedReachableaffected

Manual Override Behaviour

When an operator overrides state, the service:

  1. Resets the lattice to Unknown.
  2. Applies the minimal evidence sequence to reach the target state (e.g., ConfirmedReachable = StaticReachable + RuntimeObserved).
  3. Sets confidence to the midpoint of the target state’s confidence range.
  4. Returns a warning when overriding from ConfirmedReachable or ConfirmedUnreachable, since these are high-certainty states.

DI Registration

AddReachabilityCore() (in StellaOps.Reachability.Core/ServiceCollectionExtensions.cs) registers ILatticeTriageService → LatticeTriageService (singleton, via TryAddSingleton). Note: the ReachGraph WebService Program.cs does not call AddReachabilityCore() and does not register the triage service — it wires IReachabilityIndex, IReachGraphAdapter, and ISignalsAdapter directly. The lattice triage service is therefore a library-level capability with no HTTP endpoint in the ReachGraph WebService today; consumers that need it must register and host it themselves.

Observability (OTel Metrics)

Meter: StellaOps.Reachability.Core.LatticeTriage (created via the injected IMeterFactory).

MetricTypeDescription
stellaops.lattice.triage.entries_created_totalCounterEntries created
stellaops.lattice.triage.evidence_applied_totalCounterEvidence applications
stellaops.lattice.triage.overrides_applied_totalCounterManual overrides
stellaops.lattice.triage.resets_totalCounterLattice resets
stellaops.lattice.triage.contested_totalCounterEntries that entered the Contested state

Source: src/__Libraries/StellaOps.Reachability.Core/LatticeTriageService.cs.

Test Coverage

21 test methods (20 [Fact] + 1 [Theory] with 3 [InlineData] rows) in StellaOps.Reachability.Core.Tests/LatticeTriageServiceTests.cs:


Last updated: 2026-05-30 (doc↔code reconciliation against src/ReachGraph/, src/__Libraries/StellaOps.ReachGraph*, and src/__Libraries/StellaOps.Reachability.Core).