# Excititor Graph Overlay Contract (v1.0.0) _Updated: 2025-12-10 | Owners: Excititor Core + UI Guilds | Scope: EXCITITOR-GRAPH-21-001..005, EXCITITOR-POLICY-20-001/002, EXCITITOR-RISK-66-001_ ## Purpose Defines the graph-ready overlay built from Link-Not-Merge observations/linksets so Console, Vuln Explorer, Policy, and Risk surfaces consume a single deterministic shape. This freezes the contract for Postgres materialization and cache APIs, unblocking Sprint 0120 tasks. ## Schema - JSON Schema: `docs/modules/excititor/schemas/vex_overlay.schema.json` (draft 2020-12, schemaVersion `1.0.0`). - Required fields: `schemaVersion`, `generatedAt`, `tenant`, `purl`, `advisoryId`, `source`, `status`, `observations[]`, `provenance`. - Status enum: `affected|not_affected|under_investigation|fixed|unknown`. - Ordering: observations are sorted by `source, advisoryId, fetchedAt` (Link-Not-Merge invariant) and emitted in that order. Overlays are returned in request PURL order, then by `advisoryId`, then `source`. - Provenance: carries `linksetId`, `linksetHash`, `observationHashes[]`, optional `policyHash`, `sbomContextHash`, and `planCacheKey` for replay. ## Postgres materialization (IAppendOnlyLinksetStore) - Table `vex_overlays` (materialized cache): - Primary key: `(tenant, purl, advisory_id, source)`. - Columns: `status`, `justifications` (jsonb), `conflicts` (jsonb), `observations` (jsonb), `provenance` (jsonb), `cached_at`, `ttl_seconds`, `schema_version`. - Indexes: unique `(tenant, purl, advisory_id, source)`, plus `(tenant, cached_at)` for TTL sweeps. - Overlay rows are regenerated when linkset hash or observation hash set changes; cache evictions use `cached_at + ttl_seconds`. - Linksets and observation hashes come from the append-only linkset store (`IAppendOnlyLinksetStore`) to preserve Aggregation-Only Contract guarantees. ## API shape (Graph/Vuln Explorer) - Endpoint: `GET /v1/graph/overlays?purl=&purl=&includeJustifications=true|false`. - Response items follow `vex_overlay.schema.json`; `cache` stanza signals `cached`, `cachedAt`, and `ttlSeconds`. - Cursoring: stable order (input PURL list) with `nextPageToken` based on `(tenant, purl, advisoryId, source, generatedAt)`. - Telemetry: `excititor.graph.overlays.cache{tenant,hit}` counter; `excititor.graph.overlays.latency_ms` histogram tagged with `cached`. ## Sample (abridged) ```json { "schemaVersion": "1.0.0", "generatedAt": "2025-12-10T00:00:00Z", "tenant": "tenant-default", "purl": "pkg:maven/org.example/foo@1.2.3", "advisoryId": "GHSA-xxxx-yyyy-zzzz", "source": "ghsa", "status": "affected", "justifications": [ { "kind": "known_affected", "reason": "Upstream GHSA reports affected range <1.3.0.", "evidence": ["concelier:ghsa:obs:6561e41b3e3f4a6e9d3b91c1"], "weight": 0.8 } ], "conflicts": [ { "field": "affected.versions", "reason": "vendor_range_differs", "values": ["<1.2.0", "<=1.3.0"], "sourceIds": ["concelier:redhat:obs:...","concelier:ghsa:obs:..."] } ], "observations": [ { "id": "concelier:ghsa:obs:6561e41b3e3f4a6e9d3b91c1", "contentHash": "sha256:1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd", "fetchedAt": "2025-11-19T00:00:00Z" } ], "provenance": { "linksetId": "concelier:ghsa:linkset:6561e41b3e3f4a6e9d3b91d0", "linksetHash": "sha256:deaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead", "observationHashes": ["sha256:1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"], "policyHash": "sha256:0f7c...9ad3", "sbomContextHash": "sha256:421af53f9eeba6903098d292fbd56f98be62ea6130b5161859889bf11d699d18", "planCacheKey": "tenant-default|pkg:maven/org.example/foo@1.2.3|GHSA-xxxx-yyyy-zzzz" }, "cache": { "cached": true, "cachedAt": "2025-12-10T00:00:00Z", "ttlSeconds": 300 } } ``` ## Validation & determinism - Validate overlays against `vex_overlay.schema.json` in CI and during materialization; reject or warn when fields drift. - Deterministic ordering: input PURL order, then `advisoryId`, then `source`; observation list sorted by `source, advisoryId, fetchedAt`. - No mutation: overlays are append-only; regeneration inserts a new row/version, leaving prior cache entries for audit until TTL expires. ## Handoff - Consumers (Console, Vuln Explorer, Policy Engine, Risk) should treat `vex_overlay.schema.json` as the authoritative contract. - Offline kits must bundle the schema file and sample payloads under `docs/samples/excititor/` with SHA256 manifests. - Future schema versions must bump `schemaVersion` and add migration notes to this document and `docs/modules/excititor/architecture.md`. - Policy and Risk surfaces in WebService now read overlays directly (with claim-store fallback for policy tests) to produce lookup and risk feeds; overlay cache/store are selected per tenant (in-memory by default, Postgres `vex.graph_overlays` when configured).