Documentation
¶
Overview ¶
Package graph provides shared types and error definitions for graph processing
Package graph provides event types for graph mutation requests from rules.
Package graph provides helper functions for semantic triples-based property access. These helpers enable clean migration from dual Properties/Triples representation to pure semantic triples as the single source of truth.
Package graph provides incoming edge index types for reverse graph traversal ¶
Package graph provides request/response types for the NATS mutation and query APIs.
These types are the public contract for graph operations via NATS request/reply. External consumers (semspec, semdragon) and internal components import this package directly to build requests and parse responses.
Graph request/reply is intentionally exempt from the BaseMessage payload registry. Request/reply is point-to-point — the subject determines the handler, so type-discriminated dispatch adds no value. See docs/concepts/15-payload-registry.md for the full rationale.
Package graph provides request/response types for the NATS mutation and query APIs. See mutation_requests.go for the full package documentation.
Package graph provides query request/response contracts for the graph query system. These types are shared between handlers (producers) and clients (consumers) to ensure type safety and consistent API contracts.
Package graph provides index query data types for the graph query system.
Package graph provides predicate query data types for the graph query system.
Package graph defines the types and primitives for the SemStreams knowledge graph engine.
Package graph provides query response type aliases for common query patterns.
Package graph provides graph summary query data types.
Package graph provides query types for graph operations ¶
Package graph provides types for entity state storage in the graph system.
Index ¶
- Constants
- Variables
- func AssertOwnedBucketsClean(ctx context.Context, client *natsclient.Client, logger *slog.Logger) error
- func ClassifyStateContractError(err error) error
- func DecodeCursor(cursor string) (string, error)
- func DecodePredicateToken(token string) (string, bool)
- func EncodeCursor(key string) string
- func EncodePredicateToken(predicate string) string
- func EnsureCatalogBucket(ctx context.Context, client *natsclient.Client, name string) (jetstream.KeyValue, error)
- func FrameworkOwnedBuckets() []string
- func GetProperties(entity *EntityState) map[string]any
- func GetPropertyTriples(entity *EntityState) []message.Triple
- func GetPropertyValue(entity *EntityState, predicate string) (any, bool)
- func GetPropertyValueTyped[T any](entity *EntityState, predicate string) (T, bool)
- func GetRelationshipTriples(entity *EntityState) []message.Triple
- func HasProperty(entity *EntityState, predicate string) bool
- func IsFrameworkOwnedBucket(bucket string) bool
- func IsKVTombstone(operation jetstream.KeyValueOp) bool
- func IsStateContractError(err error) bool
- func KVCatalog() []natsclient.BucketSpec
- func MarshalEntityState(entity *EntityState) ([]byte, error)
- func MatchesAnyIDPrefix(id string, prefixes []string) bool
- func MergeTriples(existing, newer []message.Triple) []message.Triple
- func OpenCatalogBucket(ctx context.Context, client *natsclient.Client, name string) (jetstream.KeyValue, error)
- func OwnerOf(bucket string) string
- func SpecFor(name string) (natsclient.BucketSpec, bool)
- func UnmarshalEntityState(data []byte, entity *EntityState) error
- func UnmarshalEntityStateTrusted(data []byte, entity *EntityState) error
- func UnwrapQueryResponse(raw []byte) ([]byte, bool)
- func ValidateDecodedEntityIDs(entityIDs []string) error
- func ValidateDecodedEntityState(entity *EntityState) error
- func ValidateDecodedEntityStatePointers(entities []*EntityState) error
- func ValidateDecodedEntityStates(entities []EntityState) error
- func ValidateEntityPredicates(entity *EntityState) error
- func ValidateEntityStateContract(entity *EntityState) error
- type AddTripleRequest
- type AddTripleResponse
- type AddTriplesBatchRequest
- type AddTriplesBatchResponse
- type AliasData
- type AliasQueryResponse
- type BacklogStatusInputs
- type CompoundPredicateData
- type CompoundPredicateQuery
- type CompoundPredicateQueryResponse
- type ContextEntry
- type CreateEntityRequest
- type CreateEntityResponse
- type CreateEntityWithTriplesRequest
- type CreateEntityWithTriplesResponse
- type DeferReason
- type DeleteEntityRequest
- type DeleteEntityResponse
- type EntityBatchResponse
- type EntityCriteria
- type EntityPredicateContractError
- type EntityState
- type EntityStateContractError
- type EntityStateContractField
- type EntityTypeSummary
- type Event
- func NewAlertEvent(alertType, sourceEntityID string, properties map[string]any, ...) (*Event, error)
- func NewEntityCreateEvent(entityID, entityType string, properties map[string]any, metadata EventMetadata) (*Event, error)
- func NewEntityDeleteEvent(entityID, reason string, metadata EventMetadata) (*Event, error)
- func NewEntityUpdateEvent(entityID string, properties map[string]any, metadata EventMetadata) (*Event, error)
- func NewRelationshipCreateEvent(fromID, toID, relationshipType string, metadata EventMetadata) (*Event, error)
- func NewRelationshipDeleteEvent(fromID, toID, relationshipType string, metadata EventMetadata) (*Event, error)
- type EventMetadata
- type EventType
- type Graphable
- type IncomingEdge
- type IncomingEdges
- func (ie *IncomingEdges) AddIncomingEdge(edge IncomingEdge)
- func (ie *IncomingEdges) Count() int
- func (ie *IncomingEdges) GetIncomingEdgesByType(edgeType string) []IncomingEdge
- func (ie *IncomingEdges) GetIncomingEntityIDs() []string
- func (ie *IncomingEdges) HasIncomingFrom(fromEntityID string) bool
- func (ie *IncomingEdges) HasIncomingOfType(edgeType string) bool
- func (ie *IncomingEdges) RemoveIncomingEdge(fromEntityID, edgeType string)
- type IncomingEntry
- type IncomingQueryResponse
- type IncomingRelationshipsData
- type IndexStatusInputs
- type IndexStatusResponse
- type InvalidEntityPredicate
- type MissingEntity
- type MissingReason
- type MutationResponse
- type NameData
- type NameIndexEntry
- type NameIndexItem
- type NameMatch
- type OutgoingEntry
- type OutgoingQueryResponse
- type OutgoingRelationshipsData
- type PredicateData
- type PredicateListData
- type PredicateListQuery
- type PredicateListQueryResponse
- type PredicateQueryResponse
- type PredicateStatsData
- type PredicateStatsQueryResponse
- type PredicateSummary
- type PrefixQueryRequest
- type PrefixQueryResponse
- type Provider
- type QueryDirection
- type QueryResponse
- type QueryResult
- type RelationshipCriteria
- type RemoveTripleRequest
- type RemoveTripleResponse
- type StateContractError
- type StateResetReason
- type StatusReading
- type SummaryData
- type SummaryRequest
- type UpdateEntityRequest
- type UpdateEntityResponse
- type UpdateEntityWithTriplesRequest
- type UpdateEntityWithTriplesResponse
Constants ¶
const ( // Primary entity storage BucketEntityStates = "ENTITY_STATES" // Graph relationship indexes BucketPredicateIndex = "PREDICATE_INDEX" BucketIncomingIndex = "INCOMING_INDEX" BucketOutgoingIndex = "OUTGOING_INDEX" // Lookup indexes BucketAliasIndex = "ALIAS_INDEX" // BucketNameIndex maps a normalized (case-folded) human-readable name/title // to the entities carrying it, for deterministic name→ranked-IDs lookup // (graph.query.byName, gh#376). Complements ALIAS_INDEX, which excludes // display-name (AliasTypeLabel) predicates. BucketNameIndex = "NAME_INDEX" // BucketEntitySuffixIndex maps an entity-ID suffix to the full 6-part ID(s) // carrying it, for partial-ID resolution. Created and owned exclusively by // graph-ingest (component.go); a member of FrameworkOwnedBuckets so a generic // rule update_kv cannot mutate it (closed by framework-owned-bucket-guards). BucketEntitySuffixIndex = "ENTITY_SUFFIX_INDEX" BucketSpatialIndex = "SPATIAL_INDEX" BucketTemporalIndex = "TEMPORAL_INDEX" // BucketTemporalIndexReverse maps entityID -> current temporal bucket key. // It lets graph-index-temporal remove an entity's stale event from its prior // time bucket when the entity is re-indexed (observed-time changed) or deleted, // so a range query never returns an entity from a bucket it has since left. BucketTemporalIndexReverse = "TEMPORAL_INDEX_REVERSE" BucketContextIndex = "CONTEXT_INDEX" // Semantic tier buckets BucketEmbeddingIndex = "EMBEDDING_INDEX" BucketEmbeddingDedup = "EMBEDDING_DEDUP" BucketCommunityIndex = "COMMUNITY_INDEX" // BucketCommunitySummaries holds LLM-generated community summaries, written // ONLY by the graph-clustering enhancement worker and keyed by // {level}.{membership_hash} (content-addressed). It is deliberately SEPARATE // from COMMUNITY_INDEX: the detector owns the partition (COMMUNITY_INDEX), // the worker owns the LLM prose (this bucket), so a lagging worker can never // clobber a fresher partition or resurrect a Prune-deleted community (ADR-087). BucketCommunitySummaries = "COMMUNITY_SUMMARIES" BucketAnomalyIndex = "ANOMALY_INDEX" // Structural tier buckets BucketStructuralIndex = "STRUCTURAL_INDEX" // Operational buckets BucketComponentStatus = "COMPONENT_STATUS" // BucketGraphIngestAppliedSeq is graph-ingest's ADR-072 redelivery-guard // durable tier: `(entityID/streamName) → last-applied stream sequence`. // Created and owned exclusively by graph-ingest (processor/graph-ingest); // a member of FrameworkOwnedBuckets so a generic rule update_kv cannot forge // a sequence stamp and silently reopen the redelivery overwrite the guard // closes (framework-owned-bucket-guards F2, #715). It is correctness-critical // no-eviction state, so the retention sweep covers it. BucketGraphIngestAppliedSeq = "GRAPH_INGEST_APPLIED_SEQ" // BucketGraphStatus is the ADR-083 readiness distribution bucket. Producers // (graph-index/graph-embedding) write their liveness envelope; consumers watch // it to answer "(status, fresh|unknown)". It is the single source of truth for // the bucket name — graph/readiness re-exports this constant. Its catalog // descriptor declares History 3 (readiness replay depth) and no lifecycle // retention; the acquisition seam and backstop strip only MaxAge/MaxBytes and // leave History untouched. BucketGraphStatus = "GRAPH_STATUS" // BucketOwnerClaims is the ADR-056 owner-claim registry — the single // `_registry` epoch key, written only through the ownership Registry. The // epoch write IS the registration audit trail (the KV-twofer), so its // catalog descriptor declares History 10 to answer "who registered what, // when" across recent deploys — and NO TTL (a TTL would age out the durable // epoch between deploys). BucketOwnerClaims = "OWNER_CLAIMS" // BucketOwnerPresence holds ADR-056 owner liveness heartbeats. Its catalog // descriptor declares bounded-ttl retention (ownership.PresenceTTL): the // TTL IS the liveness contract — a presence key not re-bumped within the // window ages out so a crashed owning lease frees — and the acquisition // seam converges to it rather than stripping it. BucketOwnerPresence = "OWNER_PRESENCE" // BucketStorageReport is the account storage report // (storage-observability): ONE KEY PER INVENTORIED RESOURCE, carrying that // resource's attribution, capacity, growth rate, projection, and pressure // state, written by the storage collector each collection. Every // operator-facing surface reads it rather than recomputing an inventory, so // there is one produced truth and no two surfaces can disagree. // // A resource that disappears from the account has its key DELETED by the // collector — a semantic decision, never an expiry — which is why its // catalog descriptor declares no-lifecycle retention like the rest of the // graph. Its bounded History is the restart-surviving growth series. BucketStorageReport = "STORAGE_REPORT" )
Bucket name constants for NATS KV storage
const ( IndexStateBuilding = "building" IndexStateReady = "ready" IndexStateDegraded = "degraded" IndexStateResetRequired = "reset_required" )
Index readiness states. Mirrors pkg/fusion.IndexState string values.
const ( // ErrorCodeRevisionMismatch indicates the request specified an // ExpectedRevision that didn't match the entity's current KV // revision at the time of write. Callers (notably // pkg/lifecycle.Manager.Transition) use this to drive // read-validate-retry loops. ErrorCodeRevisionMismatch = "revision_mismatch" // ErrorCodeEntityNotFound indicates the request targeted an // entity ID that does not exist in ENTITY_STATES. For // update-style requests this means the entity was never created // or was deleted concurrently. ErrorCodeEntityNotFound = "entity_not_found" // ErrorCodeEntityExists indicates a create-style request hit an // entity ID that already exists. Used by // CreateEntityWithTriples to surface create-or-fail conflicts. ErrorCodeEntityExists = "entity_already_exists" // ErrorCodeInvalidRequest indicates the request envelope failed // pre-write validation (nil entity, malformed JSON, etc.). // Callers should not retry without fixing the request. ErrorCodeInvalidRequest = "invalid_request" // ErrorCodeStructuralInvalid indicates a token in the mutation violated the // structural-identity contract — an entity ID that is not exactly 6 parts, or a // predicate that is not exactly 3 parts (domain.category.property). The token is // malformed at its source; callers must fix the token, not retry. Emitted by the // graph-ingest structural gate when enforcement is on. ErrorCodeStructuralInvalid = "structural_invalid" // ErrorCodeInternal is the catch-all for handler-internal // failures (KV transport errors, unmarshal failures on stored // state, etc.). Callers may retry as appropriate. ErrorCodeInternal = "internal" // ErrorCodeOwnerLeaseStale indicates the request's OwnerToken does not // match the live owner recorded in the owner registry for the contested // (entity, predicate) cell — the writer is either a different process or // a revived-stale incarnation of the same owner id. Callers should NOT // retry without resolving the ownership conflict. Emitted by graph-ingest's // create_with_triples / update_with_triples handlers ONLY when the // enforce_owner_lease config is set (ADR-056 PR-5). The default observe-only // posture (PR-3) meters owner_lease_mismatch_total + Warn-logs the mismatch // and commits the write instead. ErrorCodeOwnerLeaseStale = "owner_lease_stale" // ErrorCodeResourceExhausted indicates a query would exceed a server-side read // budget — e.g. a byName lookup on a name shared by more entities than the // hydration cap (gh#474 Codex P2a interim guard). The caller should narrow the // query rather than retry unchanged; the full hot-key NAME redesign is gh#381. ErrorCodeResourceExhausted = "resource_exhausted" // ErrorCodeIndexNotReady indicates a query arrived while the serving index was not // SOUND to read from — the responder or its watcher is unavailable, the index is // degraded by an unresolved required write, or it has not finished its initial // build. The motivating case is the breaking composite-key format cutover on an // in-place upgrade (gh#474 Codex P1d), where old aggregate keys are inert and the // new keyset is still materialising: serving it would advertise a smaller graph // than exists. // // NOT emitted for ordinary revision lag (narrowed by ADR-084). A healthy index that // is merely behind SERVES, reporting its view age as staleness_ms on the readiness // envelope, because coverage was never evidence of soundness and gating on it made // every write burst look like a fault (#592). Probe health with // `nats kv get GRAPH_STATUS graph-index` and read `state` + `bootstrap_complete` — // NOT the `ready` bit, which answers coverage and is false on any busy index. // // Callers retrying it should retry with backoff against the health question, and a // caller that needs "is MY write visible?" should not use this code at all: compare // the kv_revision its mutation returned against the envelope's indexed_revision, // which is the one sound per-entity check (ADR-084). ErrorCodeIndexNotReady = "index_not_ready" // a SOURCE entity that has no usable embedding to query FROM — the entity has no // embedding record, its embedding is not yet generated (status != generated), or // its stored vector is empty. It is a PER-ENTITY miss, NOT a producer-wide index // fault: the embedding index itself is sound (contrast ErrorCodeIndexNotReady), // this one source entity simply has no vector yet (e.g. an aggregation/group // entity never projected through the embedder). Class is ErrorInvalid. // // Emitted by graph-embedding's handleQuerySimilarNATS. The semantic-edge cache // (B2 §7) recognizes it as a definitive "this entity has no semantic neighbors" // and fails open to an empty set, distinct from a malformed / unknown reply which // it must count toward its coverage-threshold abort rather than cache as a hollow // empty (#662 / Codex P1#2). Consumers classify by this stable code, never by the // message text. ErrorCodeEmbeddingUnavailable = "embedding_unavailable" )
Stable failure codes carried by *errs.ClassifiedError.Code on the mutation error path (ADR-060). Reached caller-side via errors.As(err, &ce) → ce.Code; revision_mismatch additionally matches the errs.ErrRevisionMismatch sentinel via errors.Is. The set is closed — adding a value requires updating both this declaration and the graph-ingest handler that emits it.
const ( // PredStubMarker marks the entity as a referential-integrity stub. Note: // this triple PERSISTS after real birth (nothing removes it), so it is NOT a // reliable "still a stub" signal — use [EntityState.IsStub] (envelope-based). PredStubMarker = "core.identity.stub" // PredStubReferencedBy records the entity ID whose relationship triple // caused this stub to be materialized. PredStubReferencedBy = "core.identity.referenced-by" // PredStubOwner records the stub's owner identity (the referencing entity's // type key, or the framework referential producer for an untyped source). PredStubOwner = "core.identity.stub-owner" )
Stub-marker predicates carried by a referential-integrity stub. Exported so the producer (graph-ingest) and enumerating consumers share one definition.
const DefaultPrefixQueryLimit = 1000
DefaultPrefixQueryLimit is the default number of entities returned per page when the caller does not specify a limit. Matches the server-side default that existed before typed pagination was introduced.
const ErrorCodeGraphStateResetRequired = "graph_state_reset_required"
ErrorCodeGraphStateResetRequired is the cross-component code for authoritative ENTITY_STATES that cannot be interpreted by the running graph contract.
const MaxPrefixQueryLimit = DefaultPrefixQueryLimit
MaxPrefixQueryLimit is the maximum number of entities returned per page. The NATS max_payload ceiling (~1 MB) is the binding constraint: a typical EntityState with a handful of triples serialises to a few KB; at 1000 entities that is already several MB. We therefore cap count at DefaultPrefixQueryLimit and rely on the byte-budget guard (maxPrefixResponseBytes in graph-ingest) as the hard ceiling. Callers requesting more than this value are silently clamped.
Variables ¶
var ( // ErrEntityNotFound indicates the requested entity does not exist ErrEntityNotFound = errors.New("entity not found") // ErrEntityExists indicates an entity already exists (for create operations) ErrEntityExists = errors.New("entity already exists") // ErrInvalidEntityID indicates the entity ID format is invalid ErrInvalidEntityID = errors.New("invalid entity ID") // ErrInvalidEntityData indicates the entity data is malformed ErrInvalidEntityData = errors.New("invalid entity data") // ErrVersionConflict indicates concurrent modification conflict ErrVersionConflict = errors.New("entity version conflict") )
Entity errors
var ( // ErrIndexNotFound indicates the requested index does not exist ErrIndexNotFound = errors.New("index not found") // ErrIndexCorrupted indicates index data is corrupted ErrIndexCorrupted = errors.New("index corrupted") // ErrIndexUpdateFailed indicates index update operation failed ErrIndexUpdateFailed = errors.New("index update failed") // ErrInvalidIndexKey indicates the index key format is invalid ErrInvalidIndexKey = errors.New("invalid index key") )
Index errors
var ( // ErrQueryTimeout indicates query execution exceeded timeout ErrQueryTimeout = errors.New("query timeout") // ErrQueryTooComplex indicates query exceeds complexity limits ErrQueryTooComplex = errors.New("query too complex") // ErrQueryDepthExceeded indicates traversal depth limit exceeded ErrQueryDepthExceeded = errors.New("query depth exceeded") // ErrInvalidQueryParams indicates query parameters are invalid ErrInvalidQueryParams = errors.New("invalid query parameters") )
Query errors
var ( // ErrAliasNotFound indicates the requested alias does not exist ErrAliasNotFound = errors.New("alias not found") // ErrAliasExists indicates an alias already exists ErrAliasExists = errors.New("alias already exists") // ErrInvalidAlias indicates the alias format is invalid ErrInvalidAlias = errors.New("invalid alias") )
Alias errors
var ( // ErrBufferFull indicates write buffer is at capacity ErrBufferFull = errors.New("buffer full") // ErrBatchTooBig indicates batch size exceeds limits ErrBatchTooBig = errors.New("batch too big") // ErrFlushFailed indicates buffer flush operation failed ErrFlushFailed = errors.New("flush failed") )
Buffer/batch errors
var ( // ErrNotStarted indicates service is not started ErrNotStarted = errors.New("service not started") // ErrAlreadyStarted indicates service is already started ErrAlreadyStarted = errors.New("service already started") // ErrShuttingDown indicates service is shutting down ErrShuttingDown = errors.New("service shutting down") )
Service lifecycle errors
var AllDeferReasons = []DeferReason{ DeferHardStop, DeferStatusUnknown, DeferUnrecognizedState, DeferBootstrapIncomplete, }
AllDeferReasons is the closed iteration domain for the typed defer reasons a gate can return. It lives next to the const block so it is the single source of truth, exactly as AllIndexStates is for wire states: adding a reason means adding it HERE, once, and every consumer that pre-initializes counter series or validates a label set picks it up automatically.
The alternative — each consumer hand-maintaining its own copy — fails SILENTLY in the direction that matters. A counter keyed off a private list drops any reason missing from it (dropping is correct: an unbounded label is a cardinality leak), so a newly added reason would defer in production while incrementing nothing, and the dashboard would read as "no defers" rather than "unknown defers".
DeferNone is deliberately absent: it is the reason attached to a PROCEED, not a defer, and pre-initializing a "" series would publish a permanently-zero label.
var AllIndexStates = []string{ IndexStateBuilding, IndexStateReady, IndexStateDegraded, IndexStateResetRequired, }
AllIndexStates is the closed iteration domain for the one-hot readiness `state` metric published by every producer of the envelope (graph-index, graph-embedding). It lives next to the const block so it is the single source of truth: adding a new readiness state means adding it HERE, and every one-hot state gauge picks it up automatically — otherwise a new state would render as all-zeros (silent "no data", not an alertable signal).
var StubMessageType = message.Type{Domain: "core", Category: "identity.stub", Version: "v1"}
StubMessageType is the envelope of a referential-integrity stub: the entity graph-ingest materializes at a referenced-but-not-yet-born ID so a relationship's target always resolves to a node (ADR-056 Decision 4 lane-ii). A stub is PROFILE-LESS and carries only the three stub-marker triples below; no domain content and no depends_on edges.
At the referenced entity's TRUE BIRTH the real producer's write overwrites this envelope with its own MessageType — but ONLY on a re-stamping lane: fact-arrival merge (unconditional overwrite), or create_with_triples / update_with_triples carrying a NON-ZERO MessageType (preserve-when-zero, non-zero wins). A birth via triple.add / add_batch, or a zero-typed update_with_triples, does NOT re-stamp the envelope — so a producer that wants its entity to stop reading as a stub must birth it on a re-stamping lane with its real type.
Consumers that ENUMERATE entities — notably the gated-DAG executor, which reads every entity under a unit prefix — MUST treat a stub as not-yet-real: dispatching a stub would bypass dependency ordering because it carries no depends_on edges (gh#429). Use EntityState.IsStub.
Functions ¶
func AssertOwnedBucketsClean ¶
func AssertOwnedBucketsClean(ctx context.Context, client *natsclient.Client, logger *slog.Logger) error
AssertOwnedBucketsClean is the PRE-START LEGACY-DRIFT BACKSTOP for the framework KV catalog's no-lifecycle buckets (ADR-068 D1). It has exactly ONE honest job: a catalog bucket whose OWNER IS NOT DEPLOYED in this composition never has its acquisition seam called — e.g. an EMBEDDING_INDEX left behind by a prior semantic deploy when booting a statistical configuration — so one boot-time pass over the catalog strips prior-boot/out-of-band retention dirt (or fails boot closed) for those owner-absent buckets.
Everything else is the seam's job, not this pass's: every deployed owner acquires its buckets through natsclient.EnsureFrameworkBucket inside its own Start — create-or-open, reconcile to the declared policy, verify, fail that Start closed — which covers prior-boot dirt AND this boot's create-races AND post-boot dynamic re-acquisition, at the moment of acquisition. There is no post-start sweep pass anymore; deleting it is safe precisely because its justified class (created-dirty during this boot) is reconciled at creation.
Scope: only descriptors declared no-lifecycle. A bounded-ttl bucket's TTL is its declared contract (the seam converges to it; this pass must not strip it) and an unmanaged bucket carries no framework retention guarantee.
Each bucket is bound READ-ONLY / MUST-EXIST (never created) / SKIP-IF-ABSENT: a guarded bucket that does not exist cannot carry a foreign TTL, and its true owner creates it clean through the seam. The backstop therefore imposes no bucket-creation ordering and never forces a resourceless deploy to provision a bucket it does not use (feedback_unconditional_resource_wiring).
func ClassifyStateContractError ¶
ClassifyStateContractError applies the one cross-component failure shape for incompatible authoritative graph state. Non-contract errors pass through unchanged so callers cannot accidentally promote operational failures.
func DecodeCursor ¶
DecodeCursor decodes a cursor token back to its raw key. Returns empty string and no error for the empty-cursor (first-page) case.
func DecodePredicateToken ¶
DecodePredicateToken reverses EncodePredicateToken. Returns (predicate, true) on success, ("", false) when the token is not valid hex (a malformed key token the caller should skip rather than surface as data).
func EncodeCursor ¶
EncodeCursor encodes a raw key as an opaque, URL-safe cursor token.
func EncodePredicateToken ¶
EncodePredicateToken hex-encodes a predicate for use as a single key token in the INCOMING / NAME / CONTEXT composite-key reverse indexes (gh#474 Codex P1a).
Canonical predicates are validated before persistence and are NATS-key-safe, but their three semantic segments contain two dots. Encoding the raw predicate in a composite key would consume three physical tokens and change the fixed token positions and arity used by INCOMING / NAME / CONTEXT filters.
Hex (not a hash) is deliberate: it is reversible, so a reader reconstructs the exact predicate from the key with no per-row value lookup — keeping INCOMING a pure prefix key-scan on its hot path. Hex is a fixed-alphabet ([0-9a-f]), dot-free encoding, so an encoded predicate is always exactly one KV-safe token. This is only a physical codec; it provides no compatibility path and does not weaken canonical predicate validation.
Shared here (not in the graph-index package) because graph-clustering and other reverse-index readers reconstruct these keys and must decode with the identical codec.
func EnsureCatalogBucket ¶
func EnsureCatalogBucket(ctx context.Context, client *natsclient.Client, name string) (jetstream.KeyValue, error)
EnsureCatalogBucket resolves a catalog bucket by name and acquires it through the OWNER seam (natsclient.EnsureFrameworkBucket): create-or-open, reconcile to the declared policy, verify, or fail the caller's Start closed. Only a bucket's declared owner calls this; readers use OpenCatalogBucket. A name outside the catalog is an invalid-config error naming it.
func FrameworkOwnedBuckets ¶
func FrameworkOwnedBuckets() []string
FrameworkOwnedBuckets returns the buckets whose writes are owned exclusively by a framework component — the DERIVED write-policy view of the catalog (Write == WriteOwnerOnly), which the rule update_kv guard enforces at load and at runtime. There is no hand-maintained list behind this: a catalog row declared owner-only appears here with nothing to forget.
func GetProperties ¶
func GetProperties(entity *EntityState) map[string]any
GetProperties computes a properties map from entity triples. Only includes non-relationship triples (property triples). This enables backward compatibility during migration.
func GetPropertyTriples ¶
func GetPropertyTriples(entity *EntityState) []message.Triple
GetPropertyTriples returns only the property triples from entity state. These represent entity attributes/properties.
func GetPropertyValue ¶
func GetPropertyValue(entity *EntityState, predicate string) (any, bool)
GetPropertyValue retrieves a property value from entity triples by predicate. Returns the value and true if found, nil and false if not found. Only searches non-relationship triples (property triples).
func GetPropertyValueTyped ¶
func GetPropertyValueTyped[T any](entity *EntityState, predicate string) (T, bool)
GetPropertyValueTyped retrieves a property value with type assertion. Returns the typed value and true if found and type matches, zero value and false otherwise.
func GetRelationshipTriples ¶
func GetRelationshipTriples(entity *EntityState) []message.Triple
GetRelationshipTriples returns only the relationship triples from entity state. These represent edges/connections to other entities.
func HasProperty ¶
func HasProperty(entity *EntityState, predicate string) bool
HasProperty checks if entity has a property with the given predicate.
func IsFrameworkOwnedBucket ¶
IsFrameworkOwnedBucket reports whether a bucket's catalog descriptor declares owner-only writes, so a generic KV writer (rule update_kv) must not mutate it.
func IsKVTombstone ¶
func IsKVTombstone(operation jetstream.KeyValueOp) bool
IsKVTombstone reports whether an authoritative KV watch entry removes the current value. NATS emits both DEL and PURGE tombstones with empty payloads; neither is an entity document and both must drive identical cleanup paths.
func IsStateContractError ¶
IsStateContractError reports whether err carries the shared authoritative graph-reset contract, either as the in-process typed cause or as the stable classified code reconstructed across request/reply.
func KVCatalog ¶
func KVCatalog() []natsclient.BucketSpec
KVCatalog returns the full descriptor table. The slice is rebuilt per call so no caller can mutate the catalog for another.
func MarshalEntityState ¶
func MarshalEntityState(entity *EntityState) ([]byte, error)
MarshalEntityState is the authoritative in-process ENTITY_STATES persistence seam. It validates the complete final candidate before serialization.
func MatchesAnyIDPrefix ¶
MatchesAnyIDPrefix reports whether a dot-delimited entity ID falls under any of the given ID prefixes (OR-matched). An empty/nil prefixes slice means "no filter" and matches every ID — the convention already proven by graphrag.filterEntityIDsByType and the []string type-filter surfaces (ADR-071).
Matching is on a dot boundary: id matches prefix p iff id == p or id has the literal prefix p+"." So prefix "c360.semspec.source.doc" matches "c360.semspec.source.doc" and "c360.semspec.source.doc.readme" but NOT "c360.semspec.source.docker.compose" — a prefix must end on a segment boundary, never mid-segment. This mirrors the dot-prefix convention on PrefixQueryRequest (the server appends the trailing dot when it filters), so the deterministic prefix query and the NL scope filter share ONE matcher and their semantics cannot drift.
An empty string element inside a non-empty slice is treated as an explicit match-all (consistent with empty=no-filter), not as "matches only the empty ID".
func MergeTriples ¶
MergeTriples merges two triple slices by REPLACING per (subject, predicate): every existing triple whose (subject, predicate) also appears in newer is dropped, and all of newer is kept. So for a single-valued property the newer value overrides the older one, and for a MULTI-valued predicate (a relationship such as flock.neighbor, where one subject holds several triples with the same predicate) the entire prior set is replaced by newer's set — this is full-set-replace, NOT preserve-all-unique-relationships. A predicate absent from newer is preserved untouched. Callers that want to accumulate values over time must publish the complete set per write (or use a different mechanism); a partial write drops the omitted objects for that predicate. (gh#466)
func OpenCatalogBucket ¶
func OpenCatalogBucket(ctx context.Context, client *natsclient.Client, name string) (jetstream.KeyValue, error)
OpenCatalogBucket resolves a catalog bucket by name and binds it must-exist through the READER seam (natsclient.OpenFrameworkBucket): it NEVER creates and never reconciles; an absent bucket yields a classified not-ready error naming the catalog owner. A name outside the catalog is an invalid-config error naming it.
func OwnerOf ¶
OwnerOf returns the catalog Owner string for a bucket ("" when the bucket is not in the catalog), for rejection messages that should tell the operator who legitimately writes it.
func SpecFor ¶
func SpecFor(name string) (natsclient.BucketSpec, bool)
SpecFor resolves a bucket name to its catalog descriptor. The second return is false for a name outside the catalog — the F2 boot-failure signal for a configuration-supplied bucket name that resolves to nothing (an operator typo may not silently create a stray unguarded bucket).
func UnmarshalEntityState ¶
func UnmarshalEntityState(data []byte, entity *EntityState) error
UnmarshalEntityState is the authoritative decoder for ENTITY_STATES and graph-view consumers. It refuses unreadable or noncanonical stored state.
func UnmarshalEntityStateTrusted ¶
func UnmarshalEntityStateTrusted(data []byte, entity *EntityState) error
UnmarshalEntityStateTrusted decodes stored ENTITY_STATES bytes WITHOUT re-validating the canonical entity-state contract (gh#562).
It exists for exactly one caller class: the ENTITY_STATES OWNER's own read-modify-write reads — graph-ingest's CAS merge and mutation closures. Those bytes were validated by MarshalEntityState when they were written, and every RMW cycle either commits a candidate re-validated by MarshalEntityState or commits nothing at all (no-op cycles exit via sentinel before any KV write). Resident noncanonical state therefore cannot launder through a merge: it survives into the merged candidate and the write gate rejects the whole write. Read-side validation on that lane is redundant for enforcement — it only changes WHERE the failure is reported — while sitting on the per-key-serialized RMW critical path (ADR-072), where it measurably lowers the ingest ceiling.
Every other reader MUST keep UnmarshalEntityState: external and authoritative readers (query handlers, graph-view consumers, index builders, lifecycle manager), poison-detection paths (the ENTITY_STATES contract guard), and any surface exposing decoded state downstream. This decoder does NOT reject noncanonical predicates or identities — that is its contract, not a gap. Malformed JSON returns a plain decode error, not the graph-state-reset classification.
func UnwrapQueryResponse ¶
UnwrapQueryResponse removes ONE QueryResponse envelope from a marshalled query reply, reporting whether it did.
It returns (payload, true) when raw is a marshalled QueryResponse, and (raw, false) — the input, byte-for-byte — when it is not. Failing to be an envelope is the ordinary case for the query families that do not use one, so it is not an error and is not reported as one.
Why the caller must not decide this from the subject ¶
The families do not partition by envelope usage. `graph.query.summary` is served by graph-query's own handler and returns this envelope, so a prefix-gated unwrap keyed on `graph.index.query.` left it double-nested as `data.<field>.data.*` — that is gh#762, and it is the observed defect.
The property is more general than that one instance, which is why detection rather than a corrected subject list. Query handlers PROXY: graph-query's semantic, spatial, similar, temporal, entity and byName handlers forward to a downstream subject and return that reply verbatim, so a reply enveloped by one component can surface under another family's subject. No reachable proxy surfaces an envelope TODAY — this is a soundness property, not a second live bug — but it means whether a reply carries the envelope is a property of the REPLY, and any subject-keyed rule is one downstream change away from being wrong again.
The discriminator is the CLOSED key set, deliberately ¶
A reply is the envelope only when it has BOTH `data` and `timestamp` and every one of its keys is drawn from {data, request_id, timestamp}.
Detecting on `data` alone would be the dangerous form: any reply that legitimately carries a top-level `data` field would be stripped of a nesting level, turning a cosmetic projection defect into silent data loss. Timestamp carries no `omitempty`, so a real envelope always has it and the conjunction is free. Requiring the set to be CLOSED additionally means a reply bearing `data` and `timestamp` ALONGSIDE other fields is not an envelope and is left alone.
What it does NOT promise ¶
- It does NOT unwrap repeatedly. Exactly one layer is removed, because exactly one is applied by the producer. Re-testing the payload would make the number of layers removed depend on user data, so a reply whose own contents happened to match would be silently flattened.
- It does NOT validate the payload, the timestamp's type, or the request_id. Key presence is the whole discriminator.
- It does NOT report envelope-borne errors. ADR-060 removed the in-body Error field: a query reply is EITHER this success body OR a classified error on the err channel. A caller looking for an error in here is reading a field that no producer has emitted since that ADR.
func ValidateDecodedEntityIDs ¶
ValidateDecodedEntityIDs validates identity-only authoritative query replies (semantic hits, name matches, and relationship endpoints) as one unit.
func ValidateDecodedEntityState ¶
func ValidateDecodedEntityState(entity *EntityState) error
ValidateDecodedEntityState applies the authoritative graph-state failure contract to an EntityState that was decoded as part of a larger query reply. Aggregate decoders must call this before exposing any candidate downstream.
func ValidateDecodedEntityStatePointers ¶
func ValidateDecodedEntityStatePointers(entities []*EntityState) error
ValidateDecodedEntityStatePointers is the pointer-slice counterpart used by GraphRAG hydration replies. Nil entries are contract violations, not absence: the batch protocol represents absence by omitting an entity.
func ValidateDecodedEntityStates ¶
func ValidateDecodedEntityStates(entities []EntityState) error
ValidateDecodedEntityStates validates an entire value collection before its caller performs any downstream work. The indexed wrapper preserves the contract error for errors.As while identifying which candidate poisoned the reply.
func ValidateEntityPredicates ¶
func ValidateEntityPredicates(entity *EntityState) error
ValidateEntityPredicates validates the complete final EntityState candidate. It returns all unique predicate/reason pairs, sorted deterministically.
func ValidateEntityStateContract ¶
func ValidateEntityStateContract(entity *EntityState) error
ValidateEntityStateContract validates one complete final EntityState candidate. It never fills or rewrites fields; the Graphable fact lane owns its one allowed empty-subject projection convenience before this seam.
Types ¶
type AddTripleRequest ¶
type AddTripleRequest struct {
Triple message.Triple `json:"triple"`
TraceID string `json:"trace_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
AddTripleRequest adds a triple to an existing entity
type AddTripleResponse ¶
type AddTripleResponse struct {
MutationResponse
Triple *message.Triple `json:"triple,omitempty"`
Deduplicated bool `json:"deduplicated,omitempty"`
}
AddTripleResponse response for triple addition.
Deduplicated=true means the entity already carried an identical six-field tuple (subject, predicate, object, datatype, source, context), so NOTHING was committed: the KV revision, entity version, and update timestamp are unchanged and no ENTITY_STATES watcher fired. It is a success, not a failure — a producer that re-derives its facts on restart is expected to hit it.
KVRevision follows the rule in MutationResponse: on a COMMITTED add it is the revision this write's own CAS produced, and on a suppressed one it is the entity's live revision — which may be 0 if that live read failed. A caller that sees Deduplicated with KVRevision 0 has NO read-your-writes anchor from this response and must read authoritative state if it needs one. A caller tracking its own writes must not record the revision at all when Deduplicated is true: nothing was written, so the value belongs to whoever wrote last.
type AddTriplesBatchRequest ¶
type AddTriplesBatchRequest struct {
Triples []message.Triple `json:"triples"`
TraceID string `json:"trace_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
AddTriplesBatchRequest adds many triples in one request, atomically per-entity (one CAS per distinct triple.Subject). Optimised for tools that emit multiple triples in a single tool call (e.g. write_todos per ADR-036 — 5 triples per todo × N todos all on the same loop entity collapse to 1 CAS round-trip).
The batch is NOT atomic across entities — if a write to entity A succeeds but a CAS conflict on entity B exhausts retries, the response surfaces the partial-failure shape and triples for A remain written. Single-entity callers (the common case) see this as fully atomic because there is only one CAS group.
type AddTriplesBatchResponse ¶
type AddTriplesBatchResponse struct {
MutationResponse
WrittenCount int `json:"written_count"`
// Deduplicated counts submitted triples that were NOT appended because the
// target entity already carried an identical six-field tuple (subject,
// predicate, object, datatype, source, context), or because the same tuple
// appeared earlier in this same request. It exists so a caller can
// distinguish "already present" from "nothing happened" without paying an
// authoritative read-back.
//
// This is a COUNT, not a no-op flag — it is non-zero on batches that also
// committed writes (a mixed batch reports written=1, deduplicated=2). The
// "nothing committed" predicate for this response is
// `WrittenCount == 0 && len(FailedSubjects) == 0`. Do NOT pattern-match it
// against AddTripleResponse.Deduplicated, which IS a boolean no-op flag;
// gating on `Deduplicated > 0` here reintroduces the revision-claiming bug
// that flag exists to prevent.
Deduplicated int `json:"deduplicated,omitempty"`
FailedSubjects map[string]string `json:"failed_subjects,omitempty"`
}
AddTriplesBatchResponse response for batched triple addition.
ADR-060: a WHOLE-batch failure (nothing committed) is a typed error on the err channel, not a body. This body is returned (with a nil Go error) for full success AND for PARTIAL success — FailedSubjects is the partial signal:
- FailedSubjects empty → every requested subject was processed without failure. WrittenCount may be ZERO on that path: the add lane suppresses a triple whose six-field tuple the entity already carries, so an entirely duplicate batch commits nothing and reports (WrittenCount 0, no failed subjects, no error). Read Deduplicated to tell that apart from an empty request — WrittenCount + Deduplicated accounts for every triple whose subject committed. A suppressed subject NEVER appears in FailedSubjects.
- FailedSubjects non-empty → PARTIAL success: per-entity atomicity means subjects NOT in FailedSubjects DID commit (durable); subjects in FailedSubjects rolled back. The caller retries just the failed subset. This is a success body (nil err), NOT a Go error — turning partial success into an error would make a committed write look retryable.
FailedSubjects maps the failing entity IDs to their per-subject error message. WrittenCount is the number of NEWLY APPENDED triples across all entities, excluding every suppressed duplicate.
type AliasData ¶
type AliasData struct {
CanonicalID *string `json:"canonical_id"` // nil if not found
}
AliasData contains the canonical entity ID for an alias lookup.
type AliasQueryResponse ¶
type AliasQueryResponse = QueryResponse[AliasData]
AliasQueryResponse is the response type for alias resolution queries.
type BacklogStatusInputs ¶
type BacklogStatusInputs struct {
// Outstanding is total un-applied work across every bound consumer, IN MESSAGES —
// the sum of NumPending (server-side, undelivered) and NumAckPending
// (delivered, not yet acked) over all of them.
//
// THE SUM IS INVARIANT TO WHICH COUNTER HOLDS A MESSAGE, and that is why it is a
// sum rather than either half. A message moves between the two continuously —
// delivered (pending -> ack-pending), nak'd back (ack-pending -> pending),
// redelivered again — so any single counter oscillates while work is steady.
// Only the total is monotone with respect to real outstanding work. Do not
// "simplify" this to NumPending: that under-reports by the whole in-process lane
// queue (up to defaultIngestLanes(8) x ingestLaneQueueDepth(256) = 2048 messages
// held delivered-but-unacked), which is precisely the backlog gh#712 tripped over.
//
// WHY NOT THE ACK FLOOR: measured 2026-07-30 against both deployed NATS versions
// (2.10, 2.12), AckFloor.Stream does not advance past a MaxDeliver-exhausted
// message, then jumps PAST it on the next unrelated ack — reading
// permanently-not-caught-up while idle and falsely-covered under traffic. It never
// means "everything at or below this is durable". See the change's design.md D0.
//
// HONESTY BOUNDARY: Outstanding == 0 means NO OUTSTANDING WORK, not EVERYTHING WAS
// APPLIED. A MaxDeliver-parked message leaves BOTH counters (measured), so it is
// invisible here. Caught-up is a backlog claim and cannot license an absence
// claim; operator visibility for parked messages is gh#742.
Outstanding uint64
// BootstrapComplete is the producer's own initial-build latch. It is an INPUT, not
// a projection: what constitutes the initial build differs per producer (a drained
// boot sweep, a watcher replay sentinel), and only the producer can say.
BootstrapComplete bool
// BootstrapScope is the size of that initial build in the producer's unit; see the
// field of the same name on IndexStatusResponse. Passed through untouched — the
// projection never compares it to anything.
BootstrapScope uint64
// ObservationFailed reports that the producer could not read its own backlog (a
// consumer.Info() failure). It projects to degraded AND forces Ready false,
// mirroring graph-index's precedent for a failed target read
// (processor/graph-index/watermark.go:69-80): a backend fault cannot honestly
// confirm caught-up, and "building" would read as ordinary progress rather than a
// fault. See the divergence note in ComputeBacklogStatus for why this is stronger
// than ComputeIndexStatus's FailedCount handling.
//
// Outstanding may still be a PARTIAL sum when this is set (some consumers read,
// one failed). That partial is kept on Lag as an honest lower bound rather than
// zeroed — a lower bound under a degraded verdict is strictly more useful to an
// operator than a fabricated 0, and no gate proceeds on degraded anyway.
ObservationFailed bool
// OldestOutstandingAt is the JetStream timestamp of the oldest outstanding
// message. ZERO means "not computable" — the projection then leaves StalenessMs at
// 0 rather than fabricating a fresh-looking view, matching the presence encoding
// on that field.
OldestOutstandingAt time.Time
// Now is the compute instant; the zero value means time.Now().
Now time.Time
// LastSynced is the last-advance timestamp, RFC3339, for the envelope field.
LastSynced string
}
BacklogStatusInputs are the observations ComputeBacklogStatus projects into the readiness envelope. It is SEPARATE from IndexStatusInputs on purpose: the two producer shapes have disjoint inputs (a revision watermark vs. a message backlog), and merging them would make mutually-exclusive fields co-resident — an invalid state made representable in the one projection whose output gates authoritative-absence claims.
type CompoundPredicateData ¶
type CompoundPredicateData struct {
Entities []string `json:"entities"`
Operator string `json:"operator"`
Matched int `json:"matched"`
}
CompoundPredicateData contains entities matching a compound predicate query.
type CompoundPredicateQuery ¶
type CompoundPredicateQuery struct {
Predicates []string `json:"predicates"`
Operator string `json:"operator"` // "AND" or "OR"
Limit int `json:"limit,omitempty"`
}
CompoundPredicateQuery represents a query combining multiple predicates.
type CompoundPredicateQueryResponse ¶
type CompoundPredicateQueryResponse = QueryResponse[CompoundPredicateData]
CompoundPredicateQueryResponse is the response type for compound predicate queries.
type ContextEntry ¶
ContextEntry represents an entity-predicate pair indexed by context.
type CreateEntityRequest ¶
type CreateEntityRequest struct {
Entity *EntityState `json:"entity"`
TraceID string `json:"trace_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
CreateEntityRequest creates a new entity
type CreateEntityResponse ¶
type CreateEntityResponse struct {
MutationResponse
Entity *EntityState `json:"entity,omitempty"`
}
CreateEntityResponse response for entity creation
type CreateEntityWithTriplesRequest ¶
type CreateEntityWithTriplesRequest struct {
Entity *EntityState `json:"entity"`
Triples []message.Triple `json:"triples"`
// IndexingProfile optionally declares the entity's indexing profile at
// creation (ADR-054 channel b): one of "content"|"control"|"signal"|
// "trace". When set, graph-ingest stamps entity.indexing.profile from it
// (highest precedence). When empty, graph-ingest falls through to the
// Graphable IndexingProfiler, then the fallback floor. Invalid values are
// treated as absent (lenient). This is the channel the registry-only
// design lacked — rule/lifecycle/loop/memory/research-graph writers use it
// to declare intent. Only meaningful on CREATE; entity-birth is the only
// place a profile is set (see also UpdateEntityWithTriplesRequest for the
// explicit-override exception).
IndexingProfile string `json:"indexing_profile,omitempty"`
// OwnerToken is the writer's owner-identity lease token in the format
// "<owner>#<incarnation>", where <owner> is the canonical owner id
// (e.g. "rule-pack.my-pack", "mission-planner") and <incarnation> is a
// per-process boot nonce (crypto/rand hex) that fences revived-stale
// writers even when the owner id is unchanged. Empty = unowned or
// legacy writer; graph-ingest skips the lease check when empty.
// Compared at graph-ingest by a later increment (ADR-056); PR-1 stamps
// it here for observe-only transit.
OwnerToken string `json:"owner_token,omitempty"`
TraceID string `json:"trace_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
CreateEntityWithTriplesRequest creates entity with triples atomically
type CreateEntityWithTriplesResponse ¶
type CreateEntityWithTriplesResponse struct {
MutationResponse
Entity *EntityState `json:"entity,omitempty"`
TriplesAdded int `json:"triples_added"`
}
CreateEntityWithTriplesResponse response for atomic entity+triples creation
type DeferReason ¶
type DeferReason string
DeferReason is the typed cause of a defer. Its string form is the label value of the defer_total{reason} counter and of the structured defer log field, so the set is CLOSED: four reasons an operator acts on differently — a broken index, an index that has not finished building, a dead status feed, and an envelope this consumer cannot interpret. The gh#590 investigation cost three comment cycles because the defer line was a bare constant.
const ( // DeferNone is the reason attached to a proceed. DeferNone DeferReason = "" // DeferHardStop is a degraded or reset_required index: broken, not behind. DeferHardStop DeferReason = "hard_stop" // DeferStatusUnknown is a status feed the consumer cannot vouch for (never // received, or older than the freshness window). It fails closed and is never // mistaken for index state. DeferStatusUnknown DeferReason = "status_unknown" // DeferUnrecognizedState is an envelope whose State is blank or outside // AllIndexStates. It is NOT the same operator problem as a dead feed: the value // arrived and decoded, so the producer is talking — it is saying something this // consumer does not understand, which means version skew (a newer producer, a // partially-written envelope, a hand-edited key). Failing closed on it is the // whole posture of ADR-084: State became load-bearing when health replaced // coverage, so an uninterpretable State cannot be waved through as "not degraded". DeferUnrecognizedState DeferReason = "unrecognized_state" // DeferBootstrapIncomplete is a producer that has not finished its initial build // in this process lifetime — the gh#474 cutover window, where the keyset is // half-materialised and a plausible small Lag is actively misleading. It replaces // the former `empty` reason, whose TargetRevision==0 proxy was wrong in both // directions: false during a cutover, and true for the authoritatively empty // graph it then wrongly deferred. DeferBootstrapIncomplete DeferReason = "bootstrap_incomplete" )
The closed defer-reason set.
func EvaluateReadinessGate ¶
func EvaluateReadinessGate(reading StatusReading) (proceed bool, reason DeferReason)
EvaluateReadinessGate is the single home for readiness gate semantics. It answers the health question and reports whether to proceed plus the typed defer reason. How a caller REACTS to a defer (error, skip a tick, serve an honest empty envelope) stays with the caller.
Evaluation order is load-bearing:
- Unknown status short-circuits BEFORE anything else. A dead status feed is a transport fact; nothing licenses proceeding on state the consumer cannot vouch for.
- An UNRECOGNIZED State defers. The check is an allow-list over AllIndexStates, not a deny-list for degraded/reset_required: a blank or future state must not read as "not degraded, therefore healthy".
- Hard stops (degraded, reset_required) defer.
- An incomplete initial build defers likewise — this is the wire-observable gh#474 guard, and the reason bootstrap_complete had to leave the producer's process.
- Everything else proceeds, however far behind the view is. Lag is not a fault; it is a property of the answer, carried on IndexStatusResponse.StalenessMs for the consumer to stamp on its output.
Ready is deliberately absent. It is COVERAGE, and coverage answers no question this gate asks: it cannot license a proceed a health verdict already denied, and it cannot withhold one a health verdict already allowed. A caller with a specific revision in hand compares it against IndexedRevision itself.
PRODUCER INVARIANT this ordering still relies on: an envelope with Ready == true must also carry BootstrapComplete == true. Health is answered before coverage is consulted anywhere, so a producer that published a caught-up envelope while reporting an unfinished build would see every consumer defer on it forever — pointing an operator at a cutover that already finished. It survived the ADR-085 collapse: deleting the Ready fast path removed the gate's own READ of the field, not the obligation on producers, because graph-index's sticky responder gate and every consumer's IndexedRevision comparison still assume the two agree. graph-index maintains it in latchBootstrap and on its caged early-boot branch; a new producer must do the same.
type DeleteEntityRequest ¶
type DeleteEntityRequest struct {
EntityID string `json:"entity_id"`
TraceID string `json:"trace_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
DeleteEntityRequest deletes an entity
type DeleteEntityResponse ¶
type DeleteEntityResponse struct {
MutationResponse
Deleted bool `json:"deleted"`
}
DeleteEntityResponse response for entity deletion
type EntityBatchResponse ¶
type EntityBatchResponse struct {
Entities []EntityState `json:"entities"`
// Missing names the requested IDs not present in Entities, with a reason each.
// Omitted entirely when nothing was missing.
Missing []MissingEntity `json:"missing,omitempty"`
}
EntityBatchResponse is the batch-query reply. Entities keeps its field name and shape, so a consumer reading only `entities` is unaffected; `missing` is additive and omitempty, so a fully-hydrated batch is byte-identical to the pre-ADR-084 response.
type EntityCriteria ¶
type EntityCriteria struct {
EntityID string `json:"entity_id,omitempty"`
Type string `json:"type,omitempty"`
Properties map[string]any `json:"properties,omitempty"`
}
EntityCriteria represents criteria for querying entities
type EntityPredicateContractError ¶
type EntityPredicateContractError struct {
Violations []InvalidEntityPredicate `json:"violations"`
}
EntityPredicateContractError reports every unique invalid predicate in one candidate. Writers must reject the whole candidate when this error is returned.
func (*EntityPredicateContractError) Error ¶
func (e *EntityPredicateContractError) Error() string
Error implements error with deterministic ordering for logs and tests.
type EntityState ¶
type EntityState struct {
// ID is the 6-part entity identifier: org.platform.domain.system.type.instance
// Used as NATS KV key for storage and retrieval.
ID string `json:"id"`
// Triples contains all semantic facts about this entity.
// Properties, relationships, and domain-specific data are all stored as triples.
Triples []message.Triple `json:"triples"`
// StorageRef optionally points to where the full original message is stored.
// Supports "store once, reference anywhere" pattern for large payloads.
// Nil if message was not stored or storage reference not available.
StorageRef *message.StorageReference `json:"storage_ref,omitempty"`
// MessageType records the original message type that created/updated this entity.
// Provides provenance and enables filtering by message source.
MessageType message.Type `json:"message_type"`
// Version is incremented on each update for optimistic concurrency control.
Version uint64 `json:"version"`
// UpdatedAt records when this entity state was last modified.
UpdatedAt time.Time `json:"updated_at"`
}
EntityState represents complete local graph state for an entity. Triples are the single source of truth for all semantic properties.
The ID field is the 6-part entity identifier (org.platform.domain.system.type.instance) which serves as the NATS KV key for storage and retrieval.
To extract type information from the ID, use message.ParseEntityID():
eid, err := message.ParseEntityID(state.ID)
if err != nil {
return fmt.Errorf("invalid entity ID: %w", err)
}
entityType := eid.Type
func (*EntityState) Clone ¶
func (es *EntityState) Clone() *EntityState
Clone returns a deep copy of the EntityState. This is used to avoid race conditions when multiple goroutines process the same entity concurrently.
func (*EntityState) GetPropertyValue ¶
func (es *EntityState) GetPropertyValue(predicate string) (any, bool)
GetPropertyValue returns the value for a property by predicate. It checks Triples for a matching predicate and returns the Object value. Returns (value, true) if found, (nil, false) if not found.
func (*EntityState) GetTriple ¶
func (es *EntityState) GetTriple(predicate string) *message.Triple
GetTriple returns the first triple matching the given predicate. Returns nil if no matching triple is found. This helper method simplifies accessing triple-based properties.
func (EntityState) IsStub ¶
func (es EntityState) IsStub() bool
IsStub reports whether this entity is still a bare referential-integrity stub — one whose envelope is StubMessageType, i.e. no real producer has re-stamped it yet. It keys on the ENVELOPE, not the PredStubMarker triple: the triple persists after real birth, whereas the envelope is overwritten by the real producer at birth, so the envelope is the signal that correctly flips from stub → real. See gh#429 and StubMessageType for the re-stamping-lane contract.
type EntityStateContractError ¶
type EntityStateContractError struct {
Field EntityStateContractField
TripleIndex int
Err error
}
EntityStateContractError reports the first noncanonical identity-bearing field in a complete EntityState. TripleIndex is -1 for the root ID.
Precedence is root ID, subjects in slice order, explicit references in slice order, then the existing deterministic predicate contract. This keeps error classification stable without echoing rejected identity bytes.
func (*EntityStateContractError) Error ¶
func (e *EntityStateContractError) Error() string
Error implements error without exposing the rejected identity.
func (*EntityStateContractError) Unwrap ¶
func (e *EntityStateContractError) Unwrap() error
Unwrap exposes the canonical entity-ID or object-type cause.
type EntityStateContractField ¶
type EntityStateContractField string
EntityStateContractField identifies the first identity-bearing field that makes a complete EntityState candidate noncanonical.
const ( // EntityStateContractFieldID is the EntityState.ID root key. EntityStateContractFieldID EntityStateContractField = "id" // EntityStateContractFieldSubject is a persisted Triple.Subject. EntityStateContractFieldSubject EntityStateContractField = "subject" // EntityStateContractFieldReference is an explicitly marked @id object. EntityStateContractFieldReference EntityStateContractField = "reference" )
type EntityTypeSummary ¶
type EntityTypeSummary struct {
// Type is the domain.system.type triple — the bucket name.
Type string `json:"type"`
// Count is how many entities in the scanned sample fall in this
// bucket. Approximate when EntitySampleTruncated is set on the
// parent SummaryData.
Count int `json:"count"`
// Examples is up to ExamplesPerType real entity IDs from this
// bucket. The caller uses these as starting points for
// query_entity / query_relationships without having to invent IDs.
Examples []string `json:"examples"`
}
EntityTypeSummary represents one entity-type bucket — entities grouped by their 6-part ID's domain.system.type segments (parts 3.4.5 in 1-indexed terms; parts 2-4 in 0-indexed slice terms).
Examples in this codebase:
acme.platform1.agent.agentic-loop.execution.<loopID> → type "agent.agentic-loop.execution" acme.platform1.agent.web.observation.<hash> → type "agent.web.observation"
type Event ¶
type Event struct {
Type EventType `json:"type"`
EntityID string `json:"entity_id"`
TargetID string `json:"target_id"`
Properties map[string]any `json:"properties"`
Metadata EventMetadata `json:"metadata"`
Confidence float64 `json:"confidence"`
}
Event represents a graph mutation request from rules.
func NewAlertEvent ¶
func NewAlertEvent( alertType, sourceEntityID string, properties map[string]any, metadata EventMetadata, ) (*Event, error)
NewAlertEvent creates and validates a deterministic framework-owned alert entity.
func NewEntityCreateEvent ¶
func NewEntityCreateEvent( entityID, entityType string, properties map[string]any, metadata EventMetadata, ) (*Event, error)
NewEntityCreateEvent creates and validates an entity creation event.
func NewEntityDeleteEvent ¶
func NewEntityDeleteEvent(entityID, reason string, metadata EventMetadata) (*Event, error)
NewEntityDeleteEvent creates and validates an entity deletion event.
func NewEntityUpdateEvent ¶
func NewEntityUpdateEvent(entityID string, properties map[string]any, metadata EventMetadata) (*Event, error)
NewEntityUpdateEvent creates and validates an entity update event.
func NewRelationshipCreateEvent ¶
func NewRelationshipCreateEvent( fromID, toID, relationshipType string, metadata EventMetadata, ) (*Event, error)
NewRelationshipCreateEvent creates and validates a relationship creation event.
func NewRelationshipDeleteEvent ¶
func NewRelationshipDeleteEvent( fromID, toID, relationshipType string, metadata EventMetadata, ) (*Event, error)
NewRelationshipDeleteEvent creates and validates a relationship deletion event.
type EventMetadata ¶
type EventMetadata struct {
RuleName string `json:"rule_name"`
Timestamp time.Time `json:"timestamp"`
Source string `json:"source"`
Reason string `json:"reason"`
Version string `json:"version"`
}
EventMetadata contains metadata about the event source and context.
type EventType ¶
type EventType string
EventType defines types of graph events that can be emitted by rules.
const ( // EventEntityCreate represents a request to create a new entity in the graph. EventEntityCreate EventType = "entity_create" // EventEntityUpdate represents a request to update an existing entity's properties. EventEntityUpdate EventType = "entity_update" // EventEntityDelete represents a request to delete an entity from the graph. EventEntityDelete EventType = "entity_delete" // EventRelationshipCreate represents a request to create a relationship between entities. EventRelationshipCreate EventType = "relationship_create" // EventRelationshipDelete represents a request to delete a relationship between entities. EventRelationshipDelete EventType = "relationship_delete" )
type Graphable ¶
type Graphable interface {
// EntityID returns deterministic 6-part ID: org.platform.domain.system.type.instance
EntityID() string
// Triples returns all facts about this entity
Triples() []message.Triple
}
Graphable enables messages to self-declare their domain entities and relationships. This interface eliminates the need for brittle string matching in entity extraction by allowing payloads to explicitly state what entities they contain and how those entities relate to each other.
The Graphable pattern addresses a core architectural requirement: messages should contain domain expertise about the entities they represent. Rather than having infrastructure code guess what entities exist based on message types or field names, the message payload itself declares this information.
Design Benefits:
- Domain Expertise: Payloads contain knowledge about their entities
- Type Safety: No string matching or reflection-based entity detection
- Extensibility: New domains simply implement the interface
- Relationships: Explicit declaration of entity relationships
- Performance: No need for complex entity detection algorithms
- Resolution Guidance: Provides hints for entity resolution and merging
Triple-Based Design:
The Graphable interface uses a Triple-based approach where payloads return RDF-like triples (subject, predicate, object) to describe entity properties and relationships. This provides maximum flexibility while remaining simple.
Example Implementation:
type PositionPayload struct {
SystemID uint8 `json:"system_id"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Altitude float32 `json:"altitude"`
}
func (p *PositionPayload) EntityID() string {
// Return deterministic 6-part ID
return fmt.Sprintf("acme.telemetry.robotics.mavlink.drone.%d", p.SystemID)
}
func (p *PositionPayload) Triples() []message.Triple {
entityID := p.EntityID()
return []message.Triple{
{Subject: entityID, Predicate: "geo.location.latitude", Object: p.Latitude},
{Subject: entityID, Predicate: "geo.location.longitude", Object: p.Longitude},
{Subject: entityID, Predicate: "geo.location.altitude", Object: p.Altitude},
}
}
Graphable provides entity identification and semantic triples
type IncomingEdge ¶
type IncomingEdge struct {
FromEntityID string `json:"from_entity_id"`
EdgeType string `json:"edge_type"`
Weight float64 `json:"weight,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
IncomingEdge represents an edge pointing TO this entity
type IncomingEdges ¶
type IncomingEdges struct {
EntityID string `json:"entity_id"`
Incoming []IncomingEdge `json:"incoming"`
UpdatedAt time.Time `json:"updated_at"`
}
IncomingEdges tracks all entities that point TO this entity
func (*IncomingEdges) AddIncomingEdge ¶
func (ie *IncomingEdges) AddIncomingEdge(edge IncomingEdge)
AddIncomingEdge adds or updates an incoming edge
func (*IncomingEdges) Count ¶
func (ie *IncomingEdges) Count() int
Count returns the number of incoming edges
func (*IncomingEdges) GetIncomingEdgesByType ¶
func (ie *IncomingEdges) GetIncomingEdgesByType(edgeType string) []IncomingEdge
GetIncomingEdgesByType returns all incoming edges of a specific type
func (*IncomingEdges) GetIncomingEntityIDs ¶
func (ie *IncomingEdges) GetIncomingEntityIDs() []string
GetIncomingEntityIDs returns all entity IDs that have edges pointing to this entity
func (*IncomingEdges) HasIncomingFrom ¶
func (ie *IncomingEdges) HasIncomingFrom(fromEntityID string) bool
HasIncomingFrom checks if there's an incoming edge from the specified entity
func (*IncomingEdges) HasIncomingOfType ¶
func (ie *IncomingEdges) HasIncomingOfType(edgeType string) bool
HasIncomingOfType checks if there's an incoming edge of the specified type
func (*IncomingEdges) RemoveIncomingEdge ¶
func (ie *IncomingEdges) RemoveIncomingEdge(fromEntityID, edgeType string)
RemoveIncomingEdge removes an incoming edge
type IncomingEntry ¶
type IncomingEntry struct {
FromEntityID string `json:"from_entity_id"`
Predicate string `json:"predicate"`
}
IncomingEntry represents a single incoming relationship in the graph.
type IncomingQueryResponse ¶
type IncomingQueryResponse = QueryResponse[IncomingRelationshipsData]
IncomingQueryResponse is the response type for incoming relationship queries.
type IncomingRelationshipsData ¶
type IncomingRelationshipsData struct {
Relationships []IncomingEntry `json:"relationships"`
}
IncomingRelationshipsData contains incoming relationships for an entity.
type IndexStatusInputs ¶
type IndexStatusInputs struct {
// Indexed is the low-water-of-pending watermark (revlag.Watermark.Indexed).
Indexed uint64
// Target is the query-time ENTITY_STATES stream LastSeq the index must reach.
Target uint64
// Stuck is the caller's own stuck-watermark detector verdict (-> degraded).
Stuck bool
// LastSynced is the last-advance timestamp, RFC3339, for the envelope field.
LastSynced string
// IndexedAt is the KV COMMIT time of the newest revision Indexed covers
// (revlag.Watermark.IndexedAt). ZERO means "not computable" — the projection
// then leaves StalenessMs at 0 rather than fabricating a fresh-looking view.
IndexedAt time.Time
// Now is the compute instant; the zero value means time.Now(). Tests set it to
// keep the staleness projection deterministic instead of asserting on the wall
// clock.
Now time.Time
// FailedCount is the number of index entries the producer CURRENTLY holds in a
// failed terminal state. When > 0 it projects to State=degraded BEFORE the "ready
// wins" branch, UNCONDITIONALLY (not gated on Ready): a producer whose watermark
// has reached its target while holding failures is COVERED (Ready stays accurate)
// but NOT healthy (a failed record is not a usable index entry). This makes the
// shared projection finally enforce the graph-index-readiness `FailedCount > 0 →
// degraded` rule for a producer (graph-embedding) whose watermark advances past
// failures. graph-index leaves this 0 — it enforces the same rule caller-side via
// its watermark hole + applyKnownIncompleteOverrides — so its projection output is
// byte-unchanged (ADR-085, #613).
FailedCount uint64
}
IndexStatusInputs are the observations ComputeIndexStatus projects into the readiness envelope. It is a struct rather than a positional argument list because two of the inputs are time.Time (IndexedAt, Now) and adjacent same-typed parameters are a silent-swap footgun in a projection whose output gates authoritative-absence claims.
type IndexStatusResponse ¶
type IndexStatusResponse struct {
Ready bool `json:"ready"` // target>0 && indexed_revision>=target_revision
State string `json:"state"` // "building" | "ready" | "degraded" | "reset_required"
// Code and Reason carry bounded operator-actionable fatal readiness state.
// They are empty during ordinary building/ready/degraded operation.
Code string `json:"code,omitempty"`
Reason string `json:"reason,omitempty"`
// BootstrapComplete reports whether this producer has finished its INITIAL BUILD
// in the current process lifetime: enumeration of ENTITY_STATES plus replay up to
// the enumeration-time target, including the authoritatively-empty 0/0 outcome. It
// latches true once and resets to false on restart, so a restart into a format
// cutover re-gates (ADR-084 D2).
//
// It exists because health is otherwise not evaluable from the wire: an index
// halfway through a gh#474 cutover reports State=building with a plausible Lag,
// indistinguishable from an index that is merely behind. Ready cannot answer it
// (a caught-up index is bootstrapped, but a bootstrapped index under write is not
// caught up) and TargetRevision==0 cannot either (it is false during a cutover and
// wrongly deferred the empty graph).
//
// NOT omitempty, and absent reads FALSE — fail closed. An envelope from a
// pre-ADR-084 producer therefore defers every health gate until the lockstep
// upgrade lands; that is the accepted migration cost, and the explicit `false` on
// the wire keeps "old producer" distinguishable from "not yet bootstrapped" in a
// `nats kv get GRAPH_STATUS <producer>` dump.
BootstrapComplete bool `json:"bootstrap_complete"`
// IndexedRevision is the low-water-of-pending watermark: every delivered
// ENTITY_STATES revision <= this has been applied and nothing <= it is still
// in flight. A consumer that knows its own target revision can gate on
// IndexedRevision >= myRev instead of the coarse global Ready bool.
IndexedRevision uint64 `json:"indexed_revision,omitempty"`
// TargetRevision is the ENTITY_STATES stream LastSeq read at query time — the
// latest committed write the index must catch up to.
TargetRevision uint64 `json:"target_revision,omitempty"`
// Lag is TargetRevision - IndexedRevision; 0 means caught up.
Lag uint64 `json:"lag,omitempty"`
// FailedCount / FailedReasons / FirstFailureAt are the bounded failure detail a
// producer that tracks per-entity failures carries on a DEGRADED envelope so an
// operator can tell a whole-dependency outage from a few persistently-failing
// entities WITHOUT any unbounded per-entity list on the watched key (#613).
//
// All three are additive and omitempty: a producer with no failures — including
// graph-index, which enforces the same rule caller-side via its watermark hole —
// emits none, so the wire is byte-unchanged. FailedReasons is bounded to a fixed
// reason enum (a handful of keys), keeping the watched key compact on the hot KV
// path. FirstFailureAt is RFC3339. FailedCount is echoed from the projection input
// (it also drives State=degraded, see ComputeIndexStatus); the other two are set by
// the producer after the projection.
FailedCount uint64 `json:"failed_count,omitempty"`
FailedReasons map[string]uint64 `json:"failed_reasons,omitempty"`
FirstFailureAt string `json:"first_failure_at,omitempty"`
// StalenessMs is the AGE OF THE VIEW in milliseconds (ADR-083): now minus the
// KV commit time of the newest ENTITY_STATES revision the index has fully
// covered. It is the view-rate consumer's tolerance unit because it is
// invariant to write rate and coalesce_ms, where a revision count is not
// (gh#590: the correct revision bound shifted 2-4x with the coalesce dial
// alone).
//
// PRESENCE ENCODING — 0 does NOT mean "zero staleness". It means the value
// carries no information: either Ready is true (no staleness to report) or the
// producer could not compute it (nothing covered yet / an early-return hard
// stop). Any COMPUTED staleness is reported as at least 1ms, so a consumer can
// treat `StalenessMs > 0` as the presence bit and never read an absent age as
// "0ms fresh".
//
// It is REPORTED, never gating. EvaluateReadinessGate does not look at this field:
// readiness withholds an answer only for index health, and how far behind the view
// is rides on the answer instead (community detection stamps it on
// staleness_at_detection_ms). A consumer that surfaces it must carry the presence
// encoding with it — publishing a bare 0 as "caught up" is the one way to turn an
// unknown age back into a false claim.
//
// It is a FLOOR, not an oracle: a revision still undelivered server-side cannot
// age it. Total stalls surface through the wall-clock stuck detector
// (State=degraded), not through this field.
StalenessMs uint64 `json:"staleness_ms,omitempty"`
// BootstrapScope is the SIZE OF THE INITIAL BUILD this producer latched
// BootstrapComplete against, expressed in THE PRODUCER'S OWN UNIT — entities for
// an enumerating producer, messages for a backlog producer, replayed values for a
// watcher producer. It is deliberately not normalized across producers; a shared
// unit would be a fiction, and every consumer reads it alongside the key it asked
// for, so it already knows whose unit it is.
//
// It exists for exactly one distinction, which is NOT recoverable from the wire
// today for any producer: `BootstrapComplete && BootstrapScope == 0` is
// "authoritatively nothing to do" — the producer finished its initial build and
// the build was empty — as opposed to "finished a build that had work in it".
// gh#732 raises this: a consumer waiting for a bootstrap replay cannot otherwise
// tell an empty-by-truth replay from one it observed too early.
//
// THE GATE MUST NOT READ IT. EvaluateReadinessGate does not look at this field and
// must never start: the moment a verdict depends on a magnitude, this becomes a
// threshold knob and readiness stops being a health question (ADR-085 deleted
// max_staleness for the same reason). A test pins the gate's verdict as identical
// across two envelopes differing only in this field.
//
// IT LICENSES NOTHING ABOUT ABSENCE. Scope 0 says the initial build found nothing
// to do at the instant it latched — not that the underlying collection is empty,
// and not that anything published later has been seen.
BootstrapScope uint64 `json:"bootstrap_scope,omitempty"`
// Phase is the optional friendly projection (ingesting | indexing | ready);
// deferred — the numeric fields are load-bearing (ADR-066 open question).
Phase string `json:"phase,omitempty"`
Revision string `json:"revision,omitempty"` // string IndexedRevision (now populated)
LastSynced string `json:"last_synced,omitempty"` // last watermark advance (now populated)
}
IndexStatusResponse is the wire shape of the readiness envelope (gh#397, enriched by ADR-066, distributed as GRAPH_STATUS KV state by ADR-083): the deterministic-fusion honesty envelope's readiness signal.
Ready reports COVERAGE: every committed ENTITY_STATES revision <= the query-time target has been applied (revision-lag, ADR-066), not merely "indexing started" (the old sticky NAME_INDEX-non-empty signal that fired minutes before the index was populated, gh#431). It guarantees revision coverage, not last-writer-wins freshness under same-key churn (ADR-066 §1 Scope boundary).
Ready LICENSES NOTHING ABOUT ABSENCE (ADR-084). It once carried an "only Ready permits an authoritative not-found" contract; that is retired, because coverage cannot answer the question — an index caught up to every revision ever committed still knows nothing about a source that never published. No consumer may treat an empty result as an authoritative not-found under ANY envelope state.
Nor does Ready == false withhold a response any more: reads gate on HEALTH (State plus BootstrapComplete), and a healthy index that is merely behind serves while reporting StalenessMs. Under continuous write Ready is false essentially always, which is what made it a poor proxy for soundness. Its remaining jobs are a caught-up fast path for the freshness question, and being the value a caller compares its own revision against via IndexedRevision — the one sound per-entity check.
The JSON field names match pkg/fusion.IndexStatus so the fusion RetrievalClient decodes this response directly into its honesty envelope; the two structs change together.
func ComputeBacklogStatus ¶
func ComputeBacklogStatus(in BacklogStatusInputs) IndexStatusResponse
ComputeBacklogStatus builds the readiness envelope for a BACKLOG producer — one whose "caught up" is the absence of un-applied messages rather than a revision watermark. It is the second named projection beside ComputeIndexStatus, not a mode of it.
- Ready = Outstanding == 0 && BootstrapComplete.
- State = observationFailed ? "degraded" : (ready ? "ready" : "building").
- Lag = Outstanding, IN MESSAGES — a different unit from the revision-lag producers' Lag, which the spec states explicitly.
- StalenessMs = 0 when Ready or when OldestOutstandingAt is unknown, else now-OldestOutstandingAt with a 1ms floor (shared presence encoding).
WHY NOT ComputeIndexStatus: it computes Ready = target > 0 && indexed >= target, which is FALSE at 0/0 — exactly the steady state of an idle backlog producer with nothing to do. Bending it to accommodate that would also risk byte-drift in graph-index's published output, which the current spec protects.
IndexedRevision and TargetRevision are DELIBERATELY NOT SET, and both are omitempty so they stay off the wire. Those fields are contractually in the ENTITY_STATES KV revision space — ADR-084 D3 pins them as comparable to a caller's kv_revision by a test. A backlog producer consumes multiple streams whose sequence spaces are independent, so there is no single scalar revision to report; writing a stream sequence into a KV-revision field would silently corrupt every read-your-writes check in the system. A test asserts their absence on the wire.
The gate's PRODUCER INVARIANT (Ready == true implies BootstrapComplete == true) holds here by construction: BootstrapComplete is a conjunct of Ready.
func ComputeIndexStatus ¶
func ComputeIndexStatus(in IndexStatusInputs) IndexStatusResponse
ComputeIndexStatus builds the honest revision-lag readiness envelope (ADR-066, extended with age-of-view staleness by ADR-083) from an indexed watermark, the query-time target (a stream LastSeq), a stuck flag (the caller's stuck-watermark detector), a last-synced timestamp, and the commit time of the indexed floor. It is the shared PROJECTION over pkg/revlag.Watermark used by every revision-lag producer (graph-index, graph-embedding); the watermark mechanism and the per-producer stuck-detector live elsewhere.
- Ready = target > 0 && indexed >= target (no max(0,…) clamp — indexed <= target is structural in the watermark, so Lag cannot underflow).
- State = failedCount>0 ? "degraded" : (ready ? "ready" : (stuck ? "degraded" : "building")). The failure check is FIRST and unconditional, so a producer caught up over failures reports degraded, not ready — Ready still reports the (accurate) coverage, but health lives in State (#613, ADR-085). With failedCount==0 the switch is identical to the prior "ready wins" behavior, so graph-index (which passes 0) is byte-unchanged.
- StalenessMs = 0 when Ready or when IndexedAt is unknown, else now-IndexedAt clamped to a 1ms minimum (see the presence encoding on the field).
The staleness subtraction is the ONE place a NATS server commit timestamp meets a local clock; under skew it is off by the skew. That is accepted (ADR-083 D3) because the alternative — a revision count — is wrong by 2-4x under a coalesce change alone. Consumer-side FRESHNESS deliberately does not compare clocks (graph/readiness judges arrival locally).
type InvalidEntityPredicate ¶
type InvalidEntityPredicate struct {
Predicate string `json:"predicate"`
Reason vocabulary.PredicateValidationReason `json:"reason"`
}
InvalidEntityPredicate identifies one unique noncanonical predicate in an EntityState candidate. Predicate values are included for diagnostics; Reason is bounded and safe for metric labels.
type MissingEntity ¶
type MissingEntity struct {
ID string `json:"id"`
Reason MissingReason `json:"reason"`
}
MissingEntity names one requested ID that is not in the response's entity list.
type MissingReason ¶
type MissingReason string
MissingReason is why one requested ID is absent from a batch response. The set is CLOSED so a consumer can switch on it exhaustively.
const ( // MissingNotFound is a KV read that returned not-found for the key — the // overwhelmingly common case. MissingNotFound MissingReason = "not_found" // MissingError is a per-ID fault that did not fail the whole call. The first-error // contract still stands for READ faults — a non-not-found backend error fails the // batch — so the only current emitter is a malformed requested ID (the empty // string), which is never looked up at all and therefore cannot honestly be called // not_found. MissingError MissingReason = "error" // MissingUnknown is synthesized CLIENT-side for a requested ID that appears in // neither the entities nor the missing list. A handler never emits it; it exists // so a client's reconciliation can be total (one entry per requested ID) without // inventing a reason it does not have. MissingUnknown MissingReason = "unknown" )
The closed missing-reason set.
type MutationResponse ¶
type MutationResponse struct {
// Degraded is true when the write COMMITTED but the post-write read-back
// failed. It is never set for a failure or a no-op. See the type docstring
// for the full contract. Omitted in JSON when false.
Degraded bool `json:"degraded,omitempty"`
// DegradedReason carries the read-back failure reason when Degraded is
// true (ADR-060: replaces the retired Error field on the degraded path).
DegradedReason string `json:"degraded_reason,omitempty"`
TraceID string `json:"trace_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
Timestamp int64 `json:"timestamp"` // Unix nano timestamp
KVRevision uint64 `json:"kv_revision,omitempty"` // KV bucket revision after write
}
MutationResponse is the base SUCCESS body for all mutations.
ADR-060: a mutation reply is EITHER a success body (this type, with a nil Go error) OR a single typed error value (*errs.ClassifiedError, carrying the wire Class + Code) — never both. The in-body error signalling (Success/Error/ErrorCode) was removed; the failure path no longer returns a body. A caller branches on the `err` from RequestClassified / RequestWithRetryClassified and reaches the machine code via errors.As(err, &ce) → ce.Code (the ErrorCode* values below) and the control-flow sentinel via errors.Is(err, errs.ErrRevisionMismatch).
Two SUCCESS shapes for entity-mutation handlers (#120):
Degraded=false → write committed, payload fully populated (Entity, KVRevision, Version, TriplesAdded all authoritative). The response is the post-write source of truth.
Degraded=true → write committed durably, but the post-write read-back failed (context cancellation, JetStream node failover, bucket re-keyed). Entity may be nil and KVRevision may be 0; DegradedReason carries the read-back failure reason. **Callers MUST NOT retry** — a retry on create returns entity_already_exists (the entity is there), and on update a CAS mismatch (the revision moved). Either re-read through a separate read path or accept the write-without-echo and continue. Gateways SHOULD return 200 OK with the Degraded flag echoed; NOT 202 — the write is COMMITTED, not pending.
Partial-batch success is also a success body, not an error: see AddTriplesBatchResponse (FailedSubjects is the partial signal).
Degraded is scoped to COMMITTED writes ONLY, on every handler. A failure and a no-op are neither, and flagging either one degraded is unsafe rather than merely imprecise: pkg/projection's AppendEvidence branches on Degraded BEFORE it inspects FailedSubjects, so a degraded-flagged failure enters committed verification and can be reported as committed.
Triple-mutation handlers (AddTriple/RemoveTriple/AddTriplesBatch) therefore resolve KVRevision from whether the write committed:
- COMMITTED → the exact revision that write's own CAS produced. NOT a value re-read afterwards: another writer can commit in between, and a caller attributing the re-read revision to itself is then holding someone else's. Never degraded on this path unless the commit somehow yielded no revision at all.
- SUPPRESSED as a duplicate, or a removal that matched nothing → the entity's live unchanged revision, so a read-your-writes check usually still resolves. That live read can itself fail, in which case KVRevision is 0: a no-op has no committed write to fall back on, and reporting it as degraded would re-open the failure above. A caller seeing Deduplicated or Removed together with KVRevision 0 therefore has NO read-your-writes anchor and must read authoritative state if it needs one. Never degraded on this path: nothing committed, so there is no echo to fail, and Deduplicated / Removed already tell the caller not to claim the revision.
- FAILED → no revision, not degraded.
- A multi-subject AddTriplesBatch → no revision, not degraded: a batch spanning entities has no single entity revision to report.
type NameData ¶
type NameData struct {
Matches []NameMatch `json:"matches"`
}
NameData contains the ranked entities for a name lookup. Order IS the ranking: exact-case matches first, then by label-predicate salience, then entity ID.
type NameIndexEntry ¶
type NameIndexEntry struct {
Name string `json:"name"` // the folded name (key source)
Items []NameIndexItem `json:"items"`
}
NameIndexEntry is the NAME_INDEX value for one normalized (case-folded) name.
type NameIndexItem ¶
type NameIndexItem struct {
EntityID string `json:"entity_id"`
Name string `json:"name"` // original-case name as stored on the entity
Predicate string `json:"predicate"` // the label predicate carrying it
Priority int `json:"priority"` // label-predicate salience (lower = higher)
}
NameIndexItem is one entity's claim on a name, stored in NAME_INDEX.
type NameMatch ¶
type NameMatch struct {
// EntityID is the matched entity (an opaque handle for the caller).
EntityID string `json:"entity_id"`
// MatchedName is the name as stored on the entity (original case).
MatchedName string `json:"matched_name"`
// Predicate is the label predicate that carried the name (e.g. dc.terms.title).
Predicate string `json:"predicate"`
// ExactCase is true when MatchedName equals the query verbatim (case included);
// such matches rank above case-folded-only matches.
ExactCase bool `json:"exact_case"`
}
NameMatch is one entity whose name/title matched a graph.query.byName lookup, carrying the signals a deterministic caller needs to calibrate trust (gh#376).
type OutgoingEntry ¶
type OutgoingEntry struct {
ToEntityID string `json:"to_entity_id"`
Predicate string `json:"predicate"`
}
OutgoingEntry represents a single outgoing relationship in the graph.
type OutgoingQueryResponse ¶
type OutgoingQueryResponse = QueryResponse[OutgoingRelationshipsData]
OutgoingQueryResponse is the response type for outgoing relationship queries.
type OutgoingRelationshipsData ¶
type OutgoingRelationshipsData struct {
Relationships []OutgoingEntry `json:"relationships"`
}
OutgoingRelationshipsData contains outgoing relationships for an entity.
type PredicateData ¶
type PredicateData struct {
Entities []string `json:"entities"`
}
PredicateData contains entities that have a specific predicate.
type PredicateListData ¶
type PredicateListData struct {
Predicates []PredicateSummary `json:"predicates"`
Total int `json:"total"`
}
PredicateListData contains all predicates with their entity counts.
type PredicateListQuery ¶
type PredicateListQuery struct {
Namespace string `json:"namespace,omitempty"`
}
PredicateListQuery is the request for graph.index.query.predicateList. Namespace, when set, scopes the listing to one exact domain or domain.category namespace (for example, "inferred.semantic" matches "inferred.semantic.high"). Omitted or empty means "list every predicate" (ADR-065).
type PredicateListQueryResponse ¶
type PredicateListQueryResponse = QueryResponse[PredicateListData]
PredicateListQueryResponse is the response type for predicate list queries.
type PredicateQueryResponse ¶
type PredicateQueryResponse = QueryResponse[PredicateData]
PredicateQueryResponse is the response type for predicate queries.
type PredicateStatsData ¶
type PredicateStatsData struct {
Predicate string `json:"predicate"`
EntityCount int `json:"entity_count"`
SampleEntities []string `json:"sample_entities"`
}
PredicateStatsData contains detailed statistics for a single predicate.
type PredicateStatsQueryResponse ¶
type PredicateStatsQueryResponse = QueryResponse[PredicateStatsData]
PredicateStatsQueryResponse is the response type for predicate stats queries.
type PredicateSummary ¶
type PredicateSummary struct {
Predicate string `json:"predicate"`
EntityCount int `json:"entity_count"`
}
PredicateSummary represents a predicate with its entity count.
type PrefixQueryRequest ¶
type PrefixQueryRequest struct {
// Prefix is a dot-delimited entity-ID prefix (e.g. "acme.ops.robotics").
// Empty string matches all entities. Do NOT include a trailing dot —
// the server appends it automatically when filtering.
Prefix string `json:"prefix"`
// Limit is the maximum number of entities to return on this page.
// <=0 means "use server default". Clamped to MaxPrefixQueryLimit.
Limit int `json:"limit,omitempty"`
// Cursor is the opaque page-continuation token from a previous response.
// Leave empty to request the first page.
Cursor string `json:"cursor,omitempty"`
}
PrefixQueryRequest is the typed request envelope for graph.query.prefix.
Cursor contract (OPAQUE):
- First page: leave Cursor empty.
- Subsequent pages: pass back the NextCursor value VERBATIM from the previous PrefixQueryResponse. Consumers MUST NOT parse, construct, or modify cursor values — the encoding may change between server versions.
- Empty NextCursor in the response means the result set is exhausted.
Limit semantics:
- <=0 → server applies DefaultPrefixQueryLimit.
- Values above MaxPrefixQueryLimit are clamped to MaxPrefixQueryLimit.
- Even within the limit, the server may return fewer entities when the accumulated response size would approach the NATS max_payload ceiling. In that case NextCursor is set so the caller can fetch the remainder.
V1 pagination caveat (keyset semantics):
- The server sorts matching keys lexicographically and uses the last returned key as the page boundary. Entities whose IDs sort before the cursor position and that are inserted between page fetches will not appear on later pages — standard keyset behaviour.
Backend note (informational, subject to change):
- NATS KV has no ranged scan; the server fetches ALL keys matching the prefix, sorts them, then slices the requested page. Large namespaces therefore incur a full key scan on every request.
type PrefixQueryResponse ¶
type PrefixQueryResponse struct {
// Entities is the full EntityState slice for this page.
Entities []EntityState `json:"entities"`
// NextCursor is the opaque continuation token; empty = exhausted.
// Old consumers that do not know about pagination safely ignore this field.
NextCursor string `json:"next_cursor,omitempty"`
}
PrefixQueryResponse is the typed response envelope for graph.query.prefix.
Entities contains the full EntityState values for this page, triples included. SemOps depends on full values being present so that downstream consumers do not need additional N+1 fetches.
NextCursor is the opaque token to pass as PrefixQueryRequest.Cursor on the next call. An empty NextCursor means the result set is exhausted. The field is omitted from the JSON output when empty, preserving wire-compatibility with old consumers that expect {"entities":[…]}.
type Provider ¶
type Provider interface {
// GetAllEntityIDs returns all entity IDs in the graph.
GetAllEntityIDs(ctx context.Context) ([]string, error)
// GetNeighbors returns the entity IDs connected to the given entity.
// direction: "outgoing", "incoming", or "both"
GetNeighbors(ctx context.Context, entityID string, direction string) ([]string, error)
// GetEdgeWeight returns the weight of the edge between two entities.
// Returns 1.0 if edge exists but has no weight, 0.0 if no edge exists.
GetEdgeWeight(ctx context.Context, fromID, toID string) (float64, error)
}
Provider abstracts the graph data source for algorithms. Used by clustering, structural indexing, and other graph operations.
type QueryDirection ¶
type QueryDirection int
QueryDirection represents the direction of a query
const ( // QueryDirectionOutgoing queries outgoing edges/relationships QueryDirectionOutgoing QueryDirection = iota // QueryDirectionIncoming queries incoming edges/relationships QueryDirectionIncoming // QueryDirectionBidirectional queries both directions QueryDirectionBidirectional )
func (QueryDirection) String ¶
func (qd QueryDirection) String() string
String returns the string representation of QueryDirection
type QueryResponse ¶
type QueryResponse[T any] struct { Data T `json:"data"` RequestID string `json:"request_id,omitempty"` Timestamp time.Time `json:"timestamp"` }
QueryResponse is the standard SUCCESS envelope for all query responses.
ADR-060: a query reply is EITHER this success body (nil Go error) OR a typed *errs.ClassifiedError on the err channel — the in-body Error field was removed. A RequestClassified caller branches on the returned err (errs.IsInvalid / IsTransient, errors.As → ce.Code); success unmarshals here.
func NewQueryResponse ¶
func NewQueryResponse[T any](data T) QueryResponse[T]
NewQueryResponse creates a successful response with the given data.
type QueryResult ¶
type QueryResult struct {
Entities []map[string]any `json:"entities"`
Relationships []map[string]any `json:"relationships"`
Count int `json:"count"`
}
QueryResult represents the result of a graph query
type RelationshipCriteria ¶
type RelationshipCriteria struct {
FromID string `json:"from_id,omitempty"`
ToID string `json:"to_id,omitempty"`
Type string `json:"type,omitempty"`
Properties map[string]any `json:"properties,omitempty"`
Direction QueryDirection `json:"direction,omitempty"`
}
RelationshipCriteria represents criteria for querying relationships
type RemoveTripleRequest ¶
type RemoveTripleRequest struct {
Subject string `json:"subject"`
Predicate string `json:"predicate"`
TraceID string `json:"trace_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
RemoveTripleRequest removes a triple from an entity
type RemoveTripleResponse ¶
type RemoveTripleResponse struct {
MutationResponse
Removed bool `json:"removed"`
}
RemoveTripleResponse response for triple removal.
Removed reports whether a write was actually COMMITTED. False means the call was a true no-op — the entity was absent, or no stored triple carried the predicate — so no revision advanced and no watcher fired. It is still a success: removal is idempotent.
A caller that attributes KVRevision to its own write MUST consult Removed first. On a no-op the reported revision is whatever the last OTHER writer produced, and claiming it as your own (the rule engine's per-rule feedback-loop tracker does exactly this) makes you drop that writer's genuine change. That no-op revision is a live read, which can itself fail and leave KVRevision 0 — so Removed=false with KVRevision 0 carries no read-your-writes anchor either; read authoritative state if you need one.
type StateContractError ¶
type StateContractError struct {
Reason StateResetReason `json:"reason"`
EntityID string `json:"entity_id,omitempty"`
Err error `json:"-"`
}
StateContractError means the authoritative graph cannot be safely read or projected. Recovery is reader-class-conditional: the authoritative read surface (graph-ingest's query lanes) scopes refusal to the poisoned entity and recovers as soon as operators repair the stored bytes (delete + recreate through canonical writes) — no restart required. Projection owners (derived views built from watched or replayed entity state) stay in sticky reset-required state and recover only by operator reset and process restart.
EntityID names the poisoned entity when the classification site knows it (snapshot sweep entry key, queried entity ID, RMW target, mutation read seam). It is additive and in-process only: the wire carries just the error class and code, so consumers reconstructing the classification from headers never see it. Err is excluded from JSON on purpose (error values do not marshal usefully); the rendered Error() string is the transport for detail.
func (*StateContractError) Error ¶
func (e *StateContractError) Error() string
Error implements error.
func (*StateContractError) Unwrap ¶
func (e *StateContractError) Unwrap() error
Unwrap exposes the structural or JSON cause.
type StateResetReason ¶
type StateResetReason string
StateResetReason is a bounded machine-readable reset cause.
const ( // GraphStateReasonUnreadableEntity means authoritative JSON cannot decode. GraphStateReasonUnreadableEntity StateResetReason = "unreadable_entity_state" // GraphStateReasonNoncanonicalPredicate means stored state violates the predicate grammar. GraphStateReasonNoncanonicalPredicate StateResetReason = "noncanonical_predicate" // GraphStateReasonNoncanonicalEntityID means the stored entity ID, a triple // subject, or an explicitly marked entity reference violates the canonical // entity identity contract. GraphStateReasonNoncanonicalEntityID StateResetReason = "noncanonical_entity_id" )
type StatusReading ¶
type StatusReading struct {
// Status is the last envelope received from the producer.
Status IndexStatusResponse
// Fresh reports that the reading is recent enough to vouch for. False means
// UNKNOWN, which fails closed — not "not ready".
Fresh bool
}
StatusReading is a consumer's local view of a producer's envelope: the envelope itself plus the one fact only the consumer knows — whether it can still vouch for the feed. It is a struct rather than positional arguments so a new consumer-local fact can be added without breaking every adopter.
graph/readiness.Reading maps onto it directly; an IN-PROCESS producer that computes its own envelope passes Fresh: true, having no transport to lose.
type SummaryData ¶
type SummaryData struct {
// TotalEntities is the count of entities actually scanned (= sum
// of EntityTypes counts). When EntitySampleTruncated is true,
// the underlying graph holds more than this.
TotalEntities int `json:"total_entities"`
// EntitySampleTruncated is true when the entity scan hit
// EntitySampleLimit before draining the bucket. The TotalEntities
// and per-type counts are then approximate.
EntitySampleTruncated bool `json:"entity_sample_truncated"`
// EntityTypes is the sorted (highest-count-first) list of type
// buckets discovered in the scanned sample.
EntityTypes []EntityTypeSummary `json:"entity_types"`
// Predicates is populated when IncludePredicates is true (default).
// Each entry mirrors PredicateSummary — predicate name + the
// entity count from the predicate index. Counts here are exact
// (the predicate index maintains them at write time); they do
// NOT inherit the entity-scan truncation.
Predicates []PredicateSummary `json:"predicates,omitempty"`
// PredicateTotal is len(Predicates). Convenience for callers
// formatting "N predicates registered" lines.
PredicateTotal int `json:"predicate_total,omitempty"`
}
SummaryData is the composite response. Caller-friendly text formatting happens at the tool-wrapper layer (agent tools / future MCP); this shape stays structured for non-LLM consumers (dashboards, audit, programmatic clients).
type SummaryRequest ¶
type SummaryRequest struct {
// IncludePredicates toggles the predicate counts section.
// Defaults to true. Setting false is useful when the caller
// already has the predicate list cached or when the response
// would otherwise exceed the gateway's body budget at large
// scales.
IncludePredicates bool `json:"include_predicates"`
// EntitySampleLimit caps how many entity IDs the type-distribution
// aggregation walks. Defaults to 2000. The resulting type counts
// are approximate when the scan is truncated; the response sets
// EntitySampleTruncated=true and TotalEntities reflects what was
// scanned, not what the graph holds. V2 candidates: write-time
// type-count index for exact counts.
EntitySampleLimit int `json:"entity_sample_limit"`
// ExamplesPerType caps how many sample entity IDs to retain per
// type bucket in the response. Defaults to 2. Example IDs are
// load-bearing for the discovery use case (semspec's bug log
// confirms models invent IDs when no examples are provided);
// keep this >= 1.
ExamplesPerType int `json:"examples_per_type"`
}
SummaryRequest is the request shape for the graph summary query. Both fields are optional with sensible V1 defaults.
type UpdateEntityRequest ¶
type UpdateEntityRequest struct {
Entity *EntityState `json:"entity"`
TraceID string `json:"trace_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
UpdateEntityRequest updates an existing entity
type UpdateEntityResponse ¶
type UpdateEntityResponse struct {
MutationResponse
Entity *EntityState `json:"entity,omitempty"`
Version int64 `json:"version,omitempty"`
}
UpdateEntityResponse response for entity update
type UpdateEntityWithTriplesRequest ¶
type UpdateEntityWithTriplesRequest struct {
Entity *EntityState `json:"entity"`
// AddTriples are applied with REPLACE-by-(subject,predicate) semantics
// (MergeTriples — gh#244): every predicate present in AddTriples has its
// prior values on the entity fully replaced. This is upsert, NOT
// incremental append. For a SINGLE-valued predicate, send the one new
// value (the old one is dropped). For a MULTI-valued predicate, send the
// FULL desired set — a partial set drops the omitted siblings. To ADD to
// a predicate while preserving existing values, use triple.add /
// triple.add_batch instead. Predicates absent from AddTriples are
// untouched.
AddTriples []message.Triple `json:"add_triples,omitempty"`
RemoveTriples []string `json:"remove_triples,omitempty"` // Predicates to delete (pure delete; runs before the AddTriples merge)
// ExpectedRevision opts into single-pass CAS-on-condition writes.
// Non-zero = require entity's current KV revision to match exactly;
// zero = existing UpdateWithRetry behavior (no CAS check on caller
// side; internal retry on conflict). Lifecycle Manager.Transition
// uses the non-zero path per ADR-049 to preserve state-machine
// correctness under concurrent writes.
ExpectedRevision uint64 `json:"expected_revision,omitempty"`
// IndexingProfile, when non-empty, is the EXPLICIT-OVERRIDE channel for an
// entity's indexing profile (ADR-054 §5). A profile is otherwise immutable
// after creation; setting it here replaces entity.indexing.profile
// (RemoveTriples + AddTriples, single-valued). Empty leaves the existing
// profile untouched. This is the only post-creation way to re-profile an
// entity — a later triple.add by a different writer must NOT change it.
IndexingProfile string `json:"indexing_profile,omitempty"`
// OwnerToken is the writer's owner-identity lease token in the format
// "<owner>#<incarnation>", where <owner> is the canonical owner id
// (e.g. "rule-pack.my-pack", "mission-planner") and <incarnation> is a
// per-process boot nonce (crypto/rand hex) that fences revived-stale
// writers even when the owner id is unchanged. Empty = unowned or
// legacy writer; graph-ingest skips the lease check when empty.
// Compared at graph-ingest by a later increment (ADR-056); PR-1 stamps
// it here for observe-only transit.
OwnerToken string `json:"owner_token,omitempty"`
TraceID string `json:"trace_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
UpdateEntityWithTriplesRequest updates entity and modifies triples atomically.
CAS-on-condition (ADR-049): when ExpectedRevision is non-zero, the handler reads the entity's current KV revision and rejects the request with a "revision mismatch" error if it doesn't match. This gives callers (notably pkg/lifecycle.Manager.Transition) the primitive needed to handle state-machine races — two writers transitioning the same entity from the same start state both pass validation but only one is allowed to commit.
When ExpectedRevision is zero (the default + the shape every existing caller uses), the handler uses internal UpdateWithRetry semantics: re-read + re-apply the delta on each retry until the CAS succeeds or the retry budget is exhausted. This is correct for the "facts accumulate" model the graph layer was built around.
Entity metadata (MessageType / Version / StorageRef) is PRESERVE-WHEN-ZERO: to change one, send a non-zero value (it wins); leaving it zero keeps the stored value, so a bare Entity{ID} delta does not erase the envelope (see graphingest.preserveStoredEntityMetadata). Corollary: there is no way to deliberately CLEAR a metadata field through this lane (gh#260) — that is the desired invariant for MessageType/Version; StorageRef is the only plausible future clear case. Triples ARE clearable: name predicates in RemoveTriples.
type UpdateEntityWithTriplesResponse ¶
type UpdateEntityWithTriplesResponse struct {
MutationResponse
Entity *EntityState `json:"entity,omitempty"`
TriplesAdded int `json:"triples_added"`
TriplesRemoved int `json:"triples_removed"`
Version int64 `json:"version,omitempty"`
}
UpdateEntityWithTriplesResponse response for atomic entity+triples update
Source Files
¶
- constants.go
- entity_predicate_contract.go
- errors.go
- events.go
- graphable.go
- helpers.go
- id_prefix.go
- incoming.go
- index_status.go
- kvcatalog.go
- mutation_requests.go
- mutation_responses.go
- owned_bucket_retention.go
- predicate_codec.go
- query_batch_types.go
- query_contracts.go
- query_index_types.go
- query_name_types.go
- query_predicate_types.go
- query_prefix_types.go
- query_response_types.go
- query_summary_types.go
- query_types.go
- readiness_gate.go
- state_contract.go
- stub.go
- types.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package clustering provides community detection algorithms and graph clustering for discovering structural patterns in the knowledge graph.
|
Package clustering provides community detection algorithms and graph clustering for discovering structural patterns in the knowledge graph. |
|
Package embedding provides vector embedding generation and caching for semantic search in the knowledge graph.
|
Package embedding provides vector embedding generation and caching for semantic search in the knowledge graph. |
|
geo
|
|
|
geojson
Package geojson provides Go types and JSON I/O for RFC 7946 GeoJSON — the canonical geometry exchange format for OGC API Connected Systems v1.0 and every other modern geospatial API SemStreams routinely interoperates with.
|
Package geojson provides Go types and JSON I/O for RFC 7946 GeoJSON — the canonical geometry exchange format for OGC API Connected Systems v1.0 and every other modern geospatial API SemStreams routinely interoperates with. |
|
Package inference provides structural anomaly detection for missing relationships.
|
Package inference provides structural anomaly detection for missing relationships. |
|
Package llm provides LLM client abstractions for OpenAI-compatible APIs.
|
Package llm provides LLM client abstractions for OpenAI-compatible APIs. |
|
Package query provides a clean interface for reading graph data from NATS KV buckets.
|
Package query provides a clean interface for reading graph data from NATS KV buckets. |
|
Package readiness owns BOTH sides of the ADR-083 readiness distribution contract.
|
Package readiness owns BOTH sides of the ADR-083 readiness distribution contract. |
|
Package structural provides structural graph indexing algorithms for query optimization and inference detection.
|
Package structural provides structural graph indexing algorithms for query optimization and inference detection. |