Documentation
¶
Overview ¶
Package projection implements the ADR-056 Decision 6 graph projection contract — the missing middle layer between "what type is flowing" (the payload registry) and "who may author these facts at runtime" (pkg/ownership).
A Contract is declared ONCE, beside the type / Graphable / gateway-resource projection code, and answers: "what graph facts does this projection emit, and in what write mode is each predicate group?" Ownership claims are then DERIVED from the contract and bound to an owner id at boot — they are not hand-maintained as a parallel registry that drifts from the projection.
payload type registration
└─ optionally declares a projection.Contract
(entity pattern · predicates × write mode · foreign-edge claims · indexing profile)
component / gateway boot
└─ projection.Bind(ctx, ownerRegistry, ownerID, contracts...)
derives the ownership.OwnerClaim/ForeignEdgeClaim set, registers it, and
returns an ownership.OwnerToken only when replace/CAS claims make the
atomic registration an owning lease; non-owning binds return zero
graph-ingest
└─ enforces the registered claims at the write boundary (a later increment)
This package is the DERIVATION + DECLARATION layer. It does not enforce anything — pkg/ownership is the enforcement substrate, and graph-ingest is the write-boundary enforcer. Manual ownership.RegisterOwner remains the low-level escape hatch for owners whose pattern is not derivable from a single payload type (the lifecycle Manager) or for migration scaffolding.
Contract-derived append-mode and foreign-edge-only registrations are durable declarations: they persist in the registry without presence, heartbeats, revival monitoring, or a lease token. If a derived registration also contains any replace/CAS claim, the whole claim set is one atomic owning lease: presence and revival monitoring apply to the entry, and stale compaction removes all of its owning and non-owning claims together.
A Contract carries no CoordinationWaiver: a legitimately-overlapping projection (the cross-product / mutual-consent case) is an explicit NON-GOAL of the contract layer and drops to manual ownership.RegisterOwner. If contract-declared overlaps ever become common, that is a signal to add waiver support here rather than let owners drift off the derivation path.
See docs/adr/056-authoritative-semantic-state.md Decision 6.
Index ¶
- Variables
- func Bind(ctx context.Context, ownerReg *ownership.Registry, owner string, ...) (ownership.OwnerToken, error)
- func BindAndHeartbeat(ctx context.Context, ownerReg *ownership.Registry, hb *ownership.Heartbeater, ...) (ownership.OwnerToken, error)
- func Derive(owner string, contracts ...Contract) (ownership.Registration, error)
- func MustRegister(c Contract)
- func Register(c Contract) error
- type AppendEvidenceMutation
- type AuthoritativeReader
- type CommitState
- type Contract
- type CreateMutation
- type EntityCreator
- type EvidenceAppender
- type ForeignEdge
- type MutationClient
- func (c *MutationClient) AppendEvidence(ctx context.Context, req AppendEvidenceMutation) (MutationReceipt, error)
- func (c *MutationClient) CreateWithTriples(ctx context.Context, req CreateMutation) (MutationReceipt, error)
- func (c *MutationClient) ReadAuthoritative(ctx context.Context, entityID string) (*graph.EntityState, error)
- func (c *MutationClient) ReplaceOwned(ctx context.Context, req ReplaceOwnedMutation) (MutationReceipt, error)
- type MutationClientConfig
- type MutationError
- type MutationErrorKind
- type MutationMetadata
- type MutationOperation
- type MutationReceipt
- type OwnedReplacer
- type PredicateGroup
- type ReplaceOwnedMutation
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidContract = errors.New("projection: invalid contract")
ErrInvalidContract is returned when a projection contract is malformed, or when deriving claims from it produces an inconsistent registration. It wraps the underlying ownership error (errors.Is still matches ownership.ErrInvalidClaim / ownership.ErrOwnershipOverlap) so callers can branch on the precise cause.
Functions ¶
func Bind ¶
func Bind(ctx context.Context, ownerReg *ownership.Registry, owner string, contracts ...Contract) (ownership.OwnerToken, error)
Bind is the component/gateway boot step: derive the owner's claims from its contracts and register them with the ownership substrate. On success it returns the owner's typed OwnerToken (ADR-056 PR-3.5) — the write-lease credential the bound owner stamps on its mutation requests. Non-owning append/foreign registrations still persist but return the zero token. Returns the zero token and ownership.ErrOwnershipOverlap (wrapped) when another owner already holds a derived cell, or the zero token and a derive/validation error.
func BindAndHeartbeat ¶
func BindAndHeartbeat(ctx context.Context, ownerReg *ownership.Registry, hb *ownership.Heartbeater, owner string, contracts ...Contract) (ownership.OwnerToken, error)
BindAndHeartbeat is Bind plus liveness enrollment for a STATIC projection owner — one registered once at boot for the whole process lifetime (a graph-writer, a rule pack), as opposed to a lifecycle.Manager workflow owner (which the Manager enrolls into its own heartbeater). It returns the bound owner's typed OwnerToken on success (the same credential Bind surfaces; the zero token on failure). A static owner that derives an owning replace/CAS OwnerClaim MUST heartbeat: RegisterOwner creates its OWNER_PRESENCE key at registration, but without ongoing heartbeats that key ages out after ownership.PresenceTTL and the next registrant compacts the owning entry out of the epoch. Non-owning append/foreign registrations create no presence and are compaction-exempt.
Enrollment happens only for a successful owning Bind: rejected owners and non-owning append/foreign registrations have no lease to keep alive. A nil hb binds without enrolling. The caller owns the Heartbeater's lifetime — build it once at the composition root (ownerReg.NewHeartbeater), run it on a shutdown-cancelled context (go hb.Run(ctx)), and pass it here for every static owning owner it binds.
func Derive ¶
func Derive(owner string, contracts ...Contract) (ownership.Registration, error)
Derive binds one or more contracts to an owner id and returns the aggregated ownership.Registration — the claims that owner registers (one OwnerClaim per group across all contracts, one ForeignEdgeClaim per foreign edge). Every contract is validated, and the AGGREGATE is checked for self-overlap (two of the owner's own contracts claiming the same cell — a config bug). Cross-OWNER overlap is left to ownership.RegisterOwner against the live epoch.
func MustRegister ¶
func MustRegister(c Contract)
MustRegister is Register that panics on error — for init()-time registration where a malformed or duplicate contract is a programming error.
Types ¶
type AppendEvidenceMutation ¶
type AppendEvidenceMutation struct {
Contract string
EntityID string
Evidence []message.Triple
Metadata MutationMetadata
}
AppendEvidenceMutation appends evidence for exactly one existing entity.
type AuthoritativeReader ¶
type AuthoritativeReader interface {
ReadAuthoritative(context.Context, string) (*graph.EntityState, error)
}
AuthoritativeReader is the least-privilege graph-ingest source-of-truth read.
type CommitState ¶
type CommitState string
CommitState reports what the client can prove about the authoritative write.
const ( CommitNotCommitted CommitState = "not-committed" CommitUnknown CommitState = "unknown" CommitCommitted CommitState = "committed" CommitVerified CommitState = "verified" )
Commit states describe progressively stronger knowledge about whether the authoritative mutation was applied.
type Contract ¶
type Contract struct {
// Name identifies the projection — typically the payload MessageType it is
// co-located with, or a logical projection name. Used as the registry key
// and in error messages.
Name string `json:"name"`
// MessageType is the payload type whose Graphable this contract projects for.
// Stamped onto every derived ForeignEdgeClaim as its Producer so the T2-seam
// reject can key on (message_type, predicate) (ADR-056 Decision 4). Optional:
// empty derives Producer-empty ("any producer") foreign edges — the
// transitional shape. A contract WITH foreign edges should name its type.
MessageType string `json:"message_type,omitempty"`
// EntityPattern is the 6-part entity-ID glob the projection writes (the
// entity it owns).
EntityPattern string `json:"entity_pattern"`
// Groups are the owned/append predicate groups by write mode.
Groups []PredicateGroup `json:"groups,omitempty"`
// BirthPredicates are a MutationClient convention: they authorize
// primary-subject facts only on CreateWithTriples and derive no ownership
// claim. The graph mutation service does not independently make these
// predicates immutable; writers outside this client contract can still
// mutate them.
BirthPredicates []string `json:"birth_predicates,omitempty"`
// ForeignEdges are the relationship edges the projection writes onto other
// entities.
ForeignEdges []ForeignEdge `json:"foreign_edges,omitempty"`
// IndexingProfile (ADR-054, optional): one of content|control|signal|trace.
IndexingProfile string `json:"indexing_profile,omitempty"`
}
Contract is the graph projection contract for one entity type / Graphable / gateway resource (ADR-056 Decision 6). It is OWNER-LESS — it declares the SHAPE of what a projection emits; the owner id is bound at Derive/Bind time, because the same projection shape may be emitted by different owners in different deployments.
func Registered ¶
func Registered() []Contract
Registered returns every registered contract, name-sorted for deterministic enumeration.
func (Contract) Validate ¶
Validate checks the contract is well-formed. It runs the contract-specific checks (name, one-write-mode-per-predicate, indexing profile) and then defers pattern / predicate / mode / foreign-edge / self-overlap validation to the ownership validators by deriving claims under a placeholder owner — so the projection layer never re-implements (and never drifts from) ownership's rules.
type CreateMutation ¶
type CreateMutation struct {
Contract string
Entity *graph.EntityState
Triples []message.Triple
Metadata MutationMetadata
}
CreateMutation creates one authoritative entity with its complete initial projection facts.
type EntityCreator ¶
type EntityCreator interface {
CreateWithTriples(context.Context, CreateMutation) (MutationReceipt, error)
}
EntityCreator is the least-privilege atomic entity-birth capability.
type EvidenceAppender ¶
type EvidenceAppender interface {
AppendEvidence(context.Context, AppendEvidenceMutation) (MutationReceipt, error)
}
EvidenceAppender is the least-privilege append-only evidence capability.
type ForeignEdge ¶
type ForeignEdge struct {
Predicate string `json:"predicate"`
Mode ownership.EdgeMode `json:"mode"`
TargetPattern string `json:"target_pattern,omitempty"`
}
ForeignEdge is a relationship edge the projection writes onto a DIFFERENT entity than the one it owns (ADR-056 Decision 4). TargetPattern is the 6-part glob of entities the edge lands on (empty = match-any).
type MutationClient ¶
type MutationClient struct {
// contains filtered or unexported fields
}
MutationClient is an immutable, concurrency-safe contract-bound graph writer.
func BindMutationClient ¶
func BindMutationClient(ctx context.Context, cfg MutationClientConfig) (*MutationClient, error)
BindMutationClient validates and copies the complete contract set before registering its ownership and liveness. The returned client never exposes or refreshes the minted owner token.
func (*MutationClient) AppendEvidence ¶
func (c *MutationClient) AppendEvidence( ctx context.Context, req AppendEvidenceMutation, ) (MutationReceipt, error)
AppendEvidence appends contract-authorized evidence to an existing entity with ambiguity-safe authoritative readback.
func (*MutationClient) CreateWithTriples ¶
func (c *MutationClient) CreateWithTriples( ctx context.Context, req CreateMutation, ) (MutationReceipt, error)
CreateWithTriples atomically creates an entity with the complete initial facts authorized by the named projection contract.
func (*MutationClient) ReadAuthoritative ¶
func (c *MutationClient) ReadAuthoritative(ctx context.Context, entityID string) (*graph.EntityState, error)
ReadAuthoritative returns the current graph-ingest source-of-truth state for entityID.
func (*MutationClient) ReplaceOwned ¶
func (c *MutationClient) ReplaceOwned( ctx context.Context, req ReplaceOwnedMutation, ) (MutationReceipt, error)
ReplaceOwned reconciles one complete replace-owned predicate group on an existing entity while preserving facts outside that group.
type MutationClientConfig ¶
type MutationClientConfig struct {
NATS *natsclient.Client
Registry *ownership.Registry
Heartbeater *ownership.Heartbeater
Owner string
Contracts []Contract
Timeout time.Duration
Retry natsclient.RetryConfig
}
MutationClientConfig binds one immutable mutation client to an owner and its complete projection-contract set.
type MutationError ¶
type MutationError struct {
Operation MutationOperation
Kind MutationErrorKind
Code string
Class errs.ErrorClass
Commit CommitState
Detail map[string]any
Err error
}
MutationError preserves the existing classified or sentinel cause while adding operation and commit knowledge.
func (*MutationError) Error ¶
func (e *MutationError) Error() string
func (*MutationError) Unwrap ¶
func (e *MutationError) Unwrap() error
Unwrap preserves existing errors.As and errors.Is behavior.
type MutationErrorKind ¶
type MutationErrorKind string
MutationErrorKind is the stable caller-facing mutation failure taxonomy.
const ( MutationInvalid MutationErrorKind = "invalid" MutationNotFound MutationErrorKind = "not-found" MutationConflict MutationErrorKind = "conflict" MutationRevisionConflict MutationErrorKind = "revision-conflict" MutationStaleOwnerToken MutationErrorKind = "stale-owner-token" MutationCommitUnknown MutationErrorKind = "commit-unknown" MutationCommittedUnverified MutationErrorKind = "committed-unverified" MutationInternal MutationErrorKind = "internal" )
Mutation error kinds form the stable caller-facing failure taxonomy.
type MutationMetadata ¶
MutationMetadata carries stable correlation and provenance for one logical mutation across every transport attempt and authoritative verification.
type MutationOperation ¶
type MutationOperation string
MutationOperation identifies the public capability that produced an outcome.
const ( MutationOperationBind MutationOperation = "bind" MutationOperationCreate MutationOperation = "create-with-triples" MutationOperationReplaceOwned MutationOperation = "replace-owned" MutationOperationAppendEvidence MutationOperation = "append-evidence" MutationOperationReadAuthoritative MutationOperation = "read-authoritative" )
Mutation operations identify each public mutation-client capability.
type MutationReceipt ¶
type MutationReceipt struct {
Entity *graph.EntityState
KVRevision uint64
Commit CommitState
Degraded bool
}
MutationReceipt is returned on both successful and commit-aware error paths.
type OwnedReplacer ¶
type OwnedReplacer interface {
ReplaceOwned(context.Context, ReplaceOwnedMutation) (MutationReceipt, error)
}
OwnedReplacer is the least-privilege owned-state reconciliation capability.
type PredicateGroup ¶
type PredicateGroup struct {
Name string `json:"name,omitempty"`
Mode ownership.WriteMode `json:"mode"`
Predicates []string `json:"predicates"`
}
PredicateGroup is a set of predicates a projection emits in ONE write mode (ADR-056 Decision 1). A predicate appears in exactly one group per contract — one predicate, one write mode per owner.
type ReplaceOwnedMutation ¶
type ReplaceOwnedMutation struct {
Contract string
Group string
EntityID string
Desired []message.Triple
Metadata MutationMetadata
}
ReplaceOwnedMutation reconciles the complete replace-owned predicate set declared by one contract.