projectprotocol

package
v2.3.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package projectprotocol defines the Project Context Protocol domain contract: skill and rule artifacts, immutable revisions, activation compare-and-swap (CAS) pointers, idempotency DTOs, canonical hashing and the deterministic effective protocol resolution with its approved limits.

Artifacts are NON-EXECUTABLE data. Kinds "skill" and "rule" are inert labels and the only accepted content type is text/markdown; nothing in this package interprets, evaluates or executes artifact content. Consumers (plugins, agents) MUST treat artifacts as documentation, never as code authority.

The package is deliberately free of persistence, HTTP/MCP and provider concerns: it depends on the Go standard library only. Store and transport layers validate through these same types and limits so local, HTTP and MCP paths cannot disagree (see the ProjectProtocolStore port in internal/domain/interfaces.go).

v1 retention policy (REQ-RET-001): revisions, activations and audit events are immutable and retained indefinitely. Deletion is exclusively the soft-delete state transition; this package defines NO hard-delete or purge operation.

Index

Constants

View Source
const (
	// MaxKeyRunes bounds an artifact key (1..128 runes).
	MaxKeyRunes = 128
	// MaxTitleRunes bounds an artifact title (1..200 runes).
	MaxTitleRunes = 200
	// MaxMessageRunes bounds a revision message (0..1024 runes).
	MaxMessageRunes = 1024
	// MaxReasonRunes bounds a soft-delete/rollback reason (1..1024 runes).
	MaxReasonRunes = 1024
	// MaxIdempotencyKeyBytes bounds an idempotency key (1..128 bytes).
	MaxIdempotencyKeyBytes = 128
	// MaxProjectRunes bounds a project reference (1..128 runes) when the
	// scope is project; the workspace default uses the empty reference.
	MaxProjectRunes = 128
	// MaxArtifactIDBytes bounds store-assigned artifact identifiers.
	MaxArtifactIDBytes = 128
	// MaxActorBytes bounds provenance actors (deleted_by, event actors):
	// 1..256 bytes of valid UTF-8.
	MaxActorBytes = 256
)

Field bounds for validated artifact fields.

View Source
const (
	// MaxArtifactContentBytes bounds artifact content measured in UTF-8
	// bytes after decode. Content is never normalized or truncated.
	MaxArtifactContentBytes = 1 << 20 // 1 MiB

	// MaxArtifactMetadataBytes bounds the canonical JSON encoding of the
	// artifact metadata object. The limit applies to canonical bytes, not
	// to the raw transport encoding.
	MaxArtifactMetadataBytes = 1 << 16 // 64 KiB

	// MaxEffectiveArtifacts bounds the number of active artifacts in the
	// effective protocol after project-over-workspace resolution.
	MaxEffectiveArtifacts = 2000

	// MaxProtocolBundleBytes bounds the canonical JSON encoding of the full
	// effective protocol bundle.
	MaxProtocolBundleBytes = 1 << 22 // 4 MiB

	// OrdinaryRequestTransportBytes is the HTTP transport cap for ordinary
	// JSON request bodies (encoded, pre-decode). Informational for
	// transport layers; semantic limits above remain authoritative.
	OrdinaryRequestTransportBytes = 1 << 20 // 1 MiB

	// LargeMutationTransportBytes is the HTTP transport cap for artifact
	// create/revision envelopes so that a valid escaped 1 MiB content plus
	// 64 KiB metadata still reaches semantic validation.
	LargeMutationTransportBytes = 8 << 20 // 8 MiB

	// MCPAbsoluteRequestBytes is the absolute encoded request cap for the
	// MCP endpoint.
	MCPAbsoluteRequestBytes = 8 << 20 // 8 MiB

	// MCPProtocolResponseTargetBytes is the MCP response cap target: it
	// must accommodate the canonical 4 MiB structured bundle plus a
	// bounded envelope.
	MCPProtocolResponseTargetBytes = 5 << 20 // 5 MiB

	// DefaultPageSize is the default page size for list operations.
	DefaultPageSize = 20

	// MaxPageSize is the maximum page size for list operations.
	MaxPageSize = 100
)

Approved v1 limits. These constants are the single source of truth shared by local CLI, HTTP, MCP and store validation; layers MUST NOT define their own divergent copies.

Exactly-at-limit values are accepted; limit+1 is rejected atomically with zero effects and no truncation (REQ-LIMIT-001..003).

View Source
const (
	ProviderHealthHealthy   = "healthy"
	ProviderHealthDegraded  = "degraded"
	ProviderHealthUnhealthy = "unhealthy"
)

Health values reuse the port health vocabulary.

View Source
const ContentTypeMarkdown = "text/markdown"

ContentTypeMarkdown is the only accepted artifact content type. Artifacts are markdown documents and never executable payloads.

Variables

View Source
var (
	ErrInvalidArtifact      = &Error{Code: ErrCodeValidation, Message: "invalid artifact"}
	ErrInvalidUTF8          = &Error{Code: ErrCodeInvalidUTF8, Message: "content is not valid UTF-8"}
	ErrDuplicateMetadataKey = &Error{Code: ErrCodeDuplicateMetadataKey, Message: "metadata contains a duplicate object key"}
	ErrContentTooLarge      = &Error{Code: ErrCodeContentTooLarge, Message: "artifact content exceeds the accepted size", Limit: MaxArtifactContentBytes}
	ErrMetadataTooLarge     = &Error{Code: ErrCodeMetadataTooLarge, Message: "canonical metadata exceeds the accepted size", Limit: MaxArtifactMetadataBytes}
	ErrEffectiveLimit       = &Error{Code: ErrCodeEffectiveLimitExceeded, Message: "effective artifact limit exceeded", Limit: MaxEffectiveArtifacts}
	ErrProtocolTooLarge     = &Error{Code: ErrCodeProtocolTooLarge, Message: "canonical protocol bundle exceeds the accepted size", Limit: MaxProtocolBundleBytes}
	ErrRevisionConflict     = &Error{Code: ErrCodeRevisionConflict, Message: "artifact changed since the expected revision"}
	ErrActivationConflict   = &Error{Code: ErrCodeActivationConflict, Message: "activation pointer changed since the expected activation revision"}
	ErrIdempotencyConflict  = &Error{Code: ErrCodeIdempotencyConflict, Message: "idempotency key was already used with a different payload"}
)

Sentinel errors for the most common failure classes. Errors carrying a dynamic Limit (limit_exceeded, *_too_large) should be constructed with NewLimitError so the exact bound travels with the error.

Functions

func CanonicalDigest

func CanonicalDigest(v any) (string, []byte, error)

CanonicalDigest returns the canonical bytes of v and their "sha256:<hex>" digest. Digests computed this way are stable across map iteration order.

func CanonicalJSON

func CanonicalJSON(v any) ([]byte, error)

CanonicalJSON encodes v into its deterministic canonical JSON form. Accepted value types: nil, bool, string, json.Number, float32/float64, signed/unsigned integers, []any, map[string]any and json.RawMessage. Other types are rejected as unsupported to avoid encoding ambiguity.

func CanonicalizeMetadata

func CanonicalizeMetadata(raw []byte) ([]byte, error)

CanonicalizeMetadata validates raw JSON metadata and returns its canonical bytes. The root must be a JSON object; duplicate keys, invalid UTF-8, unpaired surrogates and non-finite numbers are rejected. The canonical byte length must not exceed MaxArtifactMetadataBytes: exactly at the limit is accepted, one byte more is rejected without truncation.

func CanonicalizeMetadataMap

func CanonicalizeMetadataMap(m map[string]any) ([]byte, error)

CanonicalizeMetadataMap validates an in-memory metadata object and returns its canonical bytes under the same rules and limit as CanonicalizeMetadata.

func DecodeCanonicalRaw

func DecodeCanonicalRaw(raw []byte) (any, error)

DecodeCanonicalRaw decodes raw JSON bytes into canonical-ready values: duplicate object keys are rejected, trailing data is rejected, numbers are preserved as json.Number, and invalid UTF-8 input is rejected before any decoding (Go's decoder would otherwise silently replace it with U+FFFD). Unpaired \uD800-\uDFFF escapes are rejected because they decode to U+FFFD and would silently alias distinct inputs.

func DigestHex

func DigestHex(bytes []byte) string

DigestHex returns the "sha256:<hex>" digest of bytes.

func ETag

func ETag(canonical []byte) string

ETag returns the opaque quoted ETag for canonical bytes (a quoted sha256 hex string), as used for artifact ETags and protocol conditional requests.

func PageSizeBounds

func PageSizeBounds(requested int) int

PageSizeBounds validates and normalizes a requested page size. Non-positive values return DefaultPageSize; values above MaxPageSize are clamped to MaxPageSize.

func ValidateContent

func ValidateContent(content string) error

ValidateContent enforces the artifact content contract: valid UTF-8, non-empty, and at most MaxArtifactContentBytes bytes after decode. Exactly at the limit is accepted; one byte more is rejected without truncation.

func ValidateContentType

func ValidateContentType(contentType string) error

ValidateContentType enforces the single non-executable content type.

func ValidateETagShape

func ValidateETagShape(etag string) error

ValidateETagShape checks that etag is a well-formed canonical entity tag: a quoted string of exactly 64 lowercase hex characters, the shape produced by ETag() over canonical bytes. If-Match preconditions and artifact ETags share this grammar.

func ValidateIdempotencyKey

func ValidateIdempotencyKey(s string) error

ValidateIdempotencyKey enforces 1..128 printable ASCII bytes (no control characters, no invalid UTF-8).

func ValidateKey

func ValidateKey(key string) error

ValidateKey enforces the stable artifact key grammar: 1..128 runes matching [a-z0-9]a-z0-9._/-*.

func ValidateProjectRef

func ValidateProjectRef(scope Scope, project string) error

ValidateProjectRef validates the project reference for a scope. The workspace default scope must use the empty reference; the project scope requires 1..128 valid-UTF-8 runes without control characters.

func ValidateSaveArtifactInput

func ValidateSaveArtifactInput(in SaveArtifactInput) error

ValidateSaveArtifactInput checks the full creation contract, including the approved content and canonical metadata limits and the REQUIRED idempotency key: creation is idempotent per key + request digest and MUST NOT be dispatched without one. An empty project means the workspace default scope.

func ValidateTitle

func ValidateTitle(title string) error

ValidateTitle enforces 1..200 valid-UTF-8 runes.

Types

type ActivateInput

type ActivateInput struct {
	ArtifactID                 string `json:"artifact_id"`
	Revision                   int64  `json:"revision"`
	ExpectedActivationRevision int64  `json:"expected_activation_revision"`
}

ActivateInput activates one revision under activation CAS.

func (ActivateInput) Validate

func (in ActivateInput) Validate() error

Validate checks the activation request invariants.

type Activation

type Activation struct {
	ArtifactID         string    `json:"artifact_id"`
	Revision           int64     `json:"revision"`
	ActivationRevision int64     `json:"activation_revision"`
	ActivatedBy        string    `json:"activated_by"`
	ActivatedAt        time.Time `json:"activated_at"`
	Reason             string    `json:"reason,omitempty"`
}

Activation is the audited compare-and-swap pointer from an artifact to exactly one of its revisions within a scope. Rollback creates a NEW activation event pointing at a previous revision; it never rewrites history. ActivationRevision is the monotonic CAS token: concurrent activate/rollback operations must win exactly once by comparing ExpectedActivationRevision against the stored value.

func (Activation) Validate

func (a Activation) Validate() error

Validate checks the activation invariants.

type Artifact

type Artifact struct {
	ID             string `json:"id"`
	Project        string `json:"project"`
	Kind           Kind   `json:"kind"`
	Key            string `json:"key"`
	Title          string `json:"title"`
	Scope          Scope  `json:"scope"`
	Status         Status `json:"status"`
	Precedence     int32  `json:"precedence"`
	LatestRevision int64  `json:"latest_revision"`
	ActiveRevision *int64 `json:"active_revision"`
	// ActivationRevision is the monotonic compare-and-swap token for the
	// activation pointer. Every activate/rollback increments it exactly once.
	ActivationRevision int64     `json:"activation_revision"`
	ETag               string    `json:"etag"`
	CreatedAt          time.Time `json:"created_at"`
	UpdatedAt          time.Time `json:"updated_at"`

	// Soft-delete provenance (REQ-RET-001). The three fields are set exactly
	// when Status transitions to deleted and are retained indefinitely;
	// artifacts in any other status MUST NOT carry them.
	DeletedAt    *time.Time `json:"deleted_at,omitempty"`
	DeletedBy    string     `json:"deleted_by,omitempty"`
	DeleteReason string     `json:"delete_reason,omitempty"`
}

Artifact is the logical, stable artifact record. Content lives exclusively in immutable revisions; the artifact row carries identity and state.

func (Artifact) CanonicalETag

func (a Artifact) CanonicalETag() (string, error)

CanonicalETag derives the artifact's canonical entity tag from its validated state: ETag() over the canonical JSON of etagPayload. Stores MUST assign exactly this value when persisting state transitions; the derivation is deterministic and content-stable across clock jitter.

func (Artifact) Validate

func (a Artifact) Validate() error

Validate performs the full artifact invariant check for persisted or constructed artifacts.

func (Artifact) VerifyETag

func (a Artifact) VerifyETag() bool

VerifyETag recomputes the canonical ETag and reports whether it still matches, detecting any post-assignment mutation of covered state.

type ArtifactEvent

type ArtifactEvent struct {
	ID         string    `json:"id"`
	ArtifactID string    `json:"artifact_id"`
	Type       EventType `json:"type"`
	// Revision references the revision written, activated or rolled back to;
	// 0 when not applicable (e.g. soft delete).
	Revision int64 `json:"revision"`
	// ActivationRevision references the activation CAS token produced by the
	// event; 0 when not applicable.
	ActivationRevision int64     `json:"activation_revision"`
	Actor              string    `json:"actor,omitempty"`
	Reason             string    `json:"reason,omitempty"`
	CreatedAt          time.Time `json:"created_at"`
}

ArtifactEvent is one immutable audit record over an artifact. Revisions and activations carry their own detail; events provide the listable timeline surface (REQ-PAGE-001 applies: cursor pagination, limit default 20/max 100, stable ordering by created_at desc, id desc).

func (ArtifactEvent) Validate

func (e ArtifactEvent) Validate() error

Validate checks the persisted-event invariants. Stores call this when appending or loading events.

type ArtifactEventPage

type ArtifactEventPage struct {
	Items []ArtifactEvent `json:"items"`
	Page  PageInfo        `json:"page"`
}

ArtifactEventPage is one bounded page of audit events.

type ArtifactFilter

type ArtifactFilter struct {
	// Project filters by project reference; empty means the workspace
	// default scope.
	Project        string `json:"project"`
	Kind           *Kind  `json:"kind,omitempty"`
	ActiveOnly     bool   `json:"active"`
	IncludeDeleted bool   `json:"include_deleted"`
	Query          string `json:"q,omitempty"`
}

ArtifactFilter selects artifacts for listing.

type ArtifactPage

type ArtifactPage struct {
	Items []*Artifact `json:"items"`
	Page  PageInfo    `json:"page"`
}

ArtifactPage is one bounded page of artifacts.

type Bundle

type Bundle struct {
	// Canonical is the canonical JSON encoding of the protocol (bounded by
	// MaxProtocolBundleBytes) or nil when the bundle exceeded the limit.
	Canonical []byte
	// ETag is the opaque quoted entity tag of the canonical bytes.
	ETag string
	// Digest is the "sha256:<hex>" digest of the canonical bytes.
	Digest string
	// BytesLen is the canonical byte count (0 on abort).
	BytesLen int
}

Bundle is the canonical bounded encoding result of a protocol snapshot.

type Error

type Error struct {
	Code    ErrorCode
	Message string
	Limit   int64
	Detail  string
}

Error is the typed domain error. Limit carries the exact bound that was exceeded (0 when not applicable) so callers can report limits without echoing content.

func AsError

func AsError(err error) *Error

AsError normalizes any error into a *Error, wrapping unknown errors as validation failures. The original message is dropped: transport-safe context can be attached via Detail by the caller.

func NewLimitError

func NewLimitError(code ErrorCode, limit int64) *Error

NewLimitError builds a typed limit failure carrying the exact bound.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is compares by stable code so errors.As/errors.Is work across wrappers.

type ErrorCode

type ErrorCode string

ErrorCode is the stable, transport-neutral failure classification for the Project Context Protocol domain. Transport layers map these codes onto their HTTP statuses / MCP error codes; the domain itself never leaks content, keys or secrets in messages.

const (
	ErrCodeValidation             ErrorCode = "validation"
	ErrCodePayloadTooLarge        ErrorCode = "payload_too_large"
	ErrCodeContentTooLarge        ErrorCode = "content_too_large"
	ErrCodeMetadataTooLarge       ErrorCode = "metadata_too_large"
	ErrCodeInvalidUTF8            ErrorCode = "invalid_utf8"
	ErrCodeDuplicateMetadataKey   ErrorCode = "duplicate_metadata_key"
	ErrCodeEffectiveLimitExceeded ErrorCode = "effective_artifact_limit_exceeded"
	ErrCodeProtocolTooLarge       ErrorCode = "protocol_too_large"
	ErrCodeLimitExceeded          ErrorCode = "limit_exceeded"
	ErrCodeRevisionConflict       ErrorCode = "revision_conflict"
	ErrCodeActivationConflict     ErrorCode = "activation_conflict"
	ErrCodeIdempotencyConflict    ErrorCode = "idempotency_conflict"
	ErrCodeNotFound               ErrorCode = "not_found"
	ErrCodeUnsupportedType        ErrorCode = "unsupported_type"
)

type EventType

type EventType string

EventType classifies an immutable audit event on an artifact's history. Events are append-only and retained indefinitely (REQ-RET-001/002); authorized history remains listable even for soft-deleted artifacts.

const (
	// EventArtifactCreated records artifact creation with its first revision.
	EventArtifactCreated EventType = "artifact_created"
	// EventRevisionAppended records an immutable revision write.
	EventRevisionAppended EventType = "revision_appended"
	// EventActivated records an activation pointer transition.
	EventActivated EventType = "activated"
	// EventRolledBack records a rollback to a previous revision.
	EventRolledBack EventType = "rolled_back"
	// EventSoftDeleted records the soft-delete state transition.
	EventSoftDeleted EventType = "soft_deleted"
)

func (EventType) Valid

func (t EventType) Valid() bool

Valid reports whether t is an accepted event type.

type IdempotencyKey

type IdempotencyKey string

IdempotencyKey is the caller-supplied idempotency identifier for artifact creation and revision writes. Keys are compared within the principal-derived workspace/project scope, never globally.

func NewIdempotencyKey

func NewIdempotencyKey(s string) (IdempotencyKey, error)

NewIdempotencyKey validates and wraps a key.

func (IdempotencyKey) String

func (k IdempotencyKey) String() string

String returns the raw key.

type IdempotencyRecord

type IdempotencyRecord struct {
	Key           IdempotencyKey `json:"key"`
	RequestDigest string         `json:"request_digest"` // sha256:<hex> of the canonical request payload
	ArtifactID    string         `json:"artifact_id"`
	Revision      int64          `json:"revision"`
}

IdempotencyRecord is the durable evidence of a previous keyed write.

func (IdempotencyRecord) Validate

func (r IdempotencyRecord) Validate() error

Validate checks the record invariants.

type IdempotencyVerdict

type IdempotencyVerdict string

IdempotencyVerdict classifies a retry against a stored record.

const (
	// IdempotencyNew means no record exists for the key: execute the write.
	IdempotencyNew IdempotencyVerdict = "new"
	// IdempotencyReplay means the same key and payload digest were seen:
	// return the original result without re-executing.
	IdempotencyReplay IdempotencyVerdict = "replay"
	// IdempotencyConflict means the key was reused with a different payload
	// digest: fail with idempotency_conflict and mutate nothing.
	IdempotencyConflict IdempotencyVerdict = "conflict"
)

func ClassifyIdempotency

func ClassifyIdempotency(stored *IdempotencyRecord, key IdempotencyKey, requestDigest string) IdempotencyVerdict

ClassifyIdempotency applies the verdict rules: same key+digest replays, same key with a different digest conflicts, absent key is new.

type KeyConflict

type KeyConflict struct {
	Kind         Kind     `json:"kind"`
	Key          string   `json:"key"`
	Scope        Scope    `json:"scope"`
	ArtifactIDs  []string `json:"artifact_ids"`
	ResolvedByID string   `json:"resolved_by_id"`
}

KeyConflict records two or more distinct artifact records sharing one (kind,key) within the same scope. The resolver still picks a deterministic winner (precedence descending, then artifact id ascending) so resolution stays total; consumers surface the conflict for operator review.

func (KeyConflict) Validate

func (c KeyConflict) Validate() error

Validate checks the conflict-entry invariants: at least two distinct artifact ids, sorted and unique, with the deterministic winner among them. It does NOT perform the bundle-level crosscheck; Protocol.Validate does.

type Kind

type Kind string

Kind is the artifact kind label. Artifacts are non-executable data; kinds exist to organize retrieval, not to select execution behavior.

const (
	KindSkill Kind = "skill"
	KindRule  Kind = "rule"
)

func ParseKind

func ParseKind(s string) (Kind, error)

ParseKind validates a kind from untrusted input.

func (Kind) Valid

func (k Kind) Valid() bool

Valid reports whether k is an accepted kind.

type LimitWriter

type LimitWriter struct {
	// contains filtered or unexported fields
}

LimitWriter is the counting writer/hash used by every bounded canonical encoding in this package (metadata, revision digests, protocol bundles).

Contract (REQ-DOS-002, REQ-LIMIT-003):

  • the total accepted byte count never exceeds the configured limit;
  • a Write whose completion would reach limit+1 is rejected in full: it contributes zero bytes, the internal buffer is discarded (no partial output), and the writer enters a failed state;
  • once failed, Bytes returns nil and every subsequent Write fails with the same typed limit error;
  • the running SHA-256 covers exactly the accepted bytes, so a successful writer yields both the bounded canonical output and its digest/ETag.

func NewLimitWriter

func NewLimitWriter(limit int64) *LimitWriter

NewLimitWriter returns a counting writer that accepts at most limit bytes. The limit must be non-negative.

func (*LimitWriter) Bytes

func (w *LimitWriter) Bytes() []byte

Bytes returns the accepted bytes, or nil once the writer has aborted. A failed encoding therefore never exposes partial output.

func (*LimitWriter) Count

func (w *LimitWriter) Count() int64

Count returns the number of accepted bytes; it never exceeds Limit.

func (*LimitWriter) Digest

func (w *LimitWriter) Digest() string

Digest returns the "sha256:<hex>" digest of the accepted bytes.

func (*LimitWriter) ETag

func (w *LimitWriter) ETag() string

ETag returns the opaque quoted ETag of the accepted bytes.

func (*LimitWriter) Failed

func (w *LimitWriter) Failed() bool

Failed reports whether the writer aborted at the limit.

func (*LimitWriter) Limit

func (w *LimitWriter) Limit() int64

Limit returns the configured bound.

func (*LimitWriter) Write

func (w *LimitWriter) Write(p []byte) (int, error)

Write implements io.Writer with the fail-closed limit contract documented on LimitWriter.

type PageInfo

type PageInfo struct {
	NextCursor       string `json:"next_cursor"`
	HasMore          bool   `json:"has_more"`
	SnapshotRevision string `json:"snapshot_revision"`
}

PageInfo carries the pagination output contract.

type PageRequest

type PageRequest struct {
	Cursor string `json:"cursor"`
	Limit  int    `json:"limit"`
}

PageRequest is the bounded cursor pagination input. Limit is normalized by PageSizeBounds; cursors are opaque, snapshot-bound tokens produced by the store layer.

func (PageRequest) Normalize

func (p PageRequest) Normalize() PageRequest

Normalize clamps the request to the approved page bounds.

type Preconditions

type Preconditions struct {
	ExpectedRevision *int64 `json:"expected_revision,omitempty"`
	IfMatchETag      string `json:"if_match_etag,omitempty"`
}

Preconditions is the optimistic concurrency guard for artifact writes. Exactly one form may be set: ExpectedRevision compares against the stored latest revision number; IfMatchETag compares against the stored artifact ETag. A stale precondition MUST fail with revision_conflict and mutate nothing.

func (Preconditions) Validate

func (p Preconditions) Validate() error

Validate enforces the exclusive union and the canonical ETag grammar.

type Protocol

type Protocol struct {
	Project          string             `json:"project"`
	ProtocolRevision string             `json:"protocol_revision"`
	GeneratedAt      time.Time          `json:"generated_at"`
	Artifacts        []ProtocolArtifact `json:"artifacts"`
	Shadowed         []ShadowedArtifact `json:"shadowed"`
	Conflicts        []KeyConflict      `json:"conflicts"`
	// ProviderBinding is the sanitized project->provider reference summary,
	// or nil when the project has no binding. It participates in bundle
	// validation, hashing and size accounting (REQ-LIMIT-003).
	ProviderBinding *ProviderBinding `json:"provider_binding"`
}

Protocol is the deterministic effective protocol snapshot for one project. ProtocolRevision is the store-supplied opaque monotonic snapshot identifier; GeneratedAt is its creation time.

func (*Protocol) EncodeBundle

func (p *Protocol) EncodeBundle() (Bundle, error)

EncodeBundle canonicalizes the protocol through the counting writer and hashing pipeline (REQ-LIMIT-003): the snapshot is fully validated BEFORE any byte is emitted (per-artifact invariants, the effective count cap, the sanitized provider binding, and the sorted/unique/crosschecked shadowed+conflict provenance), then streamed into the 4 MiB bounded buffer. Exactly 4 MiB is accepted; one byte more aborts with protocol_too_large and no partial canonical bytes are returned.

The canonical bytes deliberately EXCLUDE generated_at: the ETag/digest are content-stable across generation time of the same snapshot (protocol_revision identifies the snapshot; conditional requests compare content, not wall clocks).

func (*Protocol) Validate

func (p *Protocol) Validate() error

Validate checks the snapshot invariants: the effective count cap, the sanitized provider binding, per-artifact semantics, strictly sorted and unique effective/(kind,key) ordering, and the FULL provenance contract on shadowed/conflicts — every entry semantically valid, sorted, unique, and crosschecked against the effective set so bundle bytes cannot carry provenance the snapshot state does not support (REQ-LIMIT-003).

type ProtocolArtifact

type ProtocolArtifact struct {
	ArtifactID  string         `json:"artifact_id"`
	Kind        Kind           `json:"kind"`
	Key         string         `json:"key"`
	Title       string         `json:"title"`
	Revision    int64          `json:"revision"`
	SourceScope Scope          `json:"source_scope"`
	ContentType string         `json:"content_type"`
	Content     string         `json:"content"`
	Metadata    map[string]any `json:"metadata"`
	Digest      string         `json:"digest"`
	Precedence  int32          `json:"precedence"`
}

ProtocolArtifact is one fully materialized artifact of the effective protocol bundle, carrying content and canonical metadata.

func (ProtocolArtifact) Validate

func (p ProtocolArtifact) Validate() error

Validate checks the bundle artifact invariants.

type ProviderBinding

type ProviderBinding struct {
	ProviderID      string `json:"provider_id"`
	Model           string `json:"model"`
	Dimension       int    `json:"dimension"`
	BindingRevision int64  `json:"binding_revision"`
	Generation      int64  `json:"generation"`
	ReindexState    string `json:"reindex_state"`
	Health          string `json:"health"`
}

ProviderBinding is the sanitized, non-secret project->provider summary carried by the effective protocol (REQ-ART-004: "provider_binding summary no secreto"). It is a versioned reference into the operator provider catalog plus its reindex state; it MUST NEVER contain credentials, tokens or raw catalog configuration.

func (ProviderBinding) Validate

func (b ProviderBinding) Validate() error

Validate checks the sanitized provider binding invariants. Every field is bounded and printable-ASCII; no secret material can be represented.

type Resolution

type Resolution struct {
	// Effective holds the winning artifact per (kind,key), ordered by kind
	// then key. Its length never exceeds MaxEffectiveArtifacts.
	Effective []ResolvableArtifact `json:"effective"`
	// Shadowed records workspace-default artifacts overridden by a project
	// artifact with the same (kind,key), ordered by kind then key.
	Shadowed []ShadowedArtifact `json:"shadowed"`
	// Conflicts records same-scope key collisions between distinct artifact
	// records (an integrity signal; keys are unique per scope by contract).
	Conflicts []KeyConflict `json:"conflicts"`
}

Resolution is the deterministic outcome of project-over-workspace resolution over active artifact summaries.

func Resolve

func Resolve(candidates []ResolvableArtifact) (Resolution, error)

Resolve computes the effective protocol summary set from active artifact candidates (REQ-ART-004).

Determinism contract:

  • for each (kind,key), a project-scope artifact wins over a workspace-default artifact; the loser is recorded in Shadowed;
  • within one scope, distinct artifact records sharing a (kind,key) are recorded in Conflicts with the deterministic winner chosen by precedence descending then artifact id ascending;
  • Effective is sorted by kind then key;
  • the effective count is capped at MaxEffectiveArtifacts: reaching limit+1 aborts with effective_artifact_limit_exceeded BEFORE any content is fetched (candidates carry no content by construction).

Candidate identity integrity: an artifact id appears at most once per snapshot. Repeated candidates whose entire summary is identical are exact duplicate rows and collapse silently (the outcome is unchanged); a repeated id with ANY differing field (key, kind, scope, precedence, revision, digest) is inconsistent data and rejected outright. This guarantees the returned provenance — conflict id lists and shadowed references — is always internally sorted, unique and crosscheck-valid for Protocol.Validate.

type ResolvableArtifact

type ResolvableArtifact struct {
	ArtifactID string `json:"artifact_id"`
	Kind       Kind   `json:"kind"`
	Key        string `json:"key"`
	Scope      Scope  `json:"scope"`
	Precedence int32  `json:"precedence"`
	Revision   int64  `json:"revision"`
	Digest     string `json:"digest"`
}

ResolvableArtifact is the manifest-level summary of one ACTIVE artifact consumed by the effective protocol resolver. It deliberately carries no content: resolution MUST count and select before any content fetch (REQ-LIMIT-002, REQ-DOS-002).

func (ResolvableArtifact) Validate

func (r ResolvableArtifact) Validate() error

Validate checks the summary invariants.

type Revision

type Revision struct {
	ArtifactID  string          `json:"artifact_id"`
	Revision    int64           `json:"revision"` // 1-based monotonic
	Title       string          `json:"title"`
	Content     string          `json:"content"`
	ContentType string          `json:"content_type"` // always text/markdown
	Metadata    json.RawMessage `json:"metadata"`     // canonical JSON object bytes
	Message     string          `json:"message,omitempty"`
	Digest      string          `json:"digest"` // sha256 of the canonical revision payload
	CreatedBy   string          `json:"created_by"`
	CreatedAt   time.Time       `json:"created_at"`
}

Revision is an immutable content snapshot of an artifact. Revisions are append-only: once constructed they are never mutated, and stores MUST NOT update or delete them (REQ-RET-001). The zero value is not valid; use NewRevision, the only constructor that computes the digest.

func NewRevision

func NewRevision(artifactID string, revisionNumber int64, in RevisionInput, createdBy string, createdAt time.Time) (Revision, error)

NewRevision validates the input and constructs the immutable revision with its canonical digest. The digest covers artifact_id, revision, title, content, content_type, canonical metadata and message; provenance fields (created_by/created_at) are deliberately excluded so the same content always yields the same digest. Metadata is stored canonically, making the digest stable across key insertion order.

func (Revision) Validate

func (r Revision) Validate() error

Validate performs the persisted-revision invariant check (stores call this when loading or writing revisions).

func (Revision) VerifyDigest

func (r Revision) VerifyDigest() bool

VerifyDigest recomputes the canonical digest and reports whether it still matches, detecting any post-construction mutation of digest-covered fields.

type RevisionInput

type RevisionInput struct {
	Title          string          `json:"title,omitempty"`
	Content        string          `json:"content"`
	ContentType    string          `json:"content_type,omitempty"`
	Metadata       json.RawMessage `json:"metadata,omitempty"`
	Message        string          `json:"message,omitempty"`
	IdempotencyKey IdempotencyKey  `json:"idempotency_key"`
}

RevisionInput is the transport-neutral payload for creating a revision. Derived fields (revision number, digest, actor, timestamp) are assigned by NewRevision at construction time, never taken from client input.

IdempotencyKey is REQUIRED on every revision write (REQ-ART-002): stores replay same key+digest requests and conflict on key reuse with a different payload. Title is optional per the API contract (title?); an empty title means "inherit the artifact title" and the store resolves it before the immutable revision is persisted.

func (RevisionInput) RequestDigest

func (in RevisionInput) RequestDigest(artifactID string) (string, error)

RequestDigest returns the canonical "sha256:<hex>" digest of the revision request payload bound to artifactID. It deliberately excludes the idempotency key itself (the key selects the comparison slot; the digest is the compared value) and provenance fields. Stores persist it in the IdempotencyRecord for replay/conflict classification.

func (RevisionInput) Validate

func (in RevisionInput) Validate() error

Validate checks the full revision input contract before construction: a required idempotency key, valid UTF-8 non-empty content within MaxArtifactContentBytes, an optional valid title, canonical metadata within MaxArtifactMetadataBytes, and the single non-executable content type.

type RevisionPage

type RevisionPage struct {
	Items []Revision `json:"items"`
	Page  PageInfo   `json:"page"`
}

RevisionPage is one bounded page of revisions.

type RollbackInput

type RollbackInput struct {
	ArtifactID                 string `json:"artifact_id"`
	ToRevision                 int64  `json:"to_revision"`
	ExpectedActivationRevision int64  `json:"expected_activation_revision"`
	Reason                     string `json:"reason"`
}

RollbackInput repoints the activation at a previous revision under activation CAS, recording an explicit reason for the audit trail.

func (RollbackInput) Validate

func (in RollbackInput) Validate() error

Validate checks the rollback request invariants.

type SaveArtifactInput

type SaveArtifactInput struct {
	Project        string          `json:"project"`
	Kind           string          `json:"kind"`
	Key            string          `json:"key"`
	Title          string          `json:"title"`
	Content        string          `json:"content"`
	ContentType    string          `json:"content_type,omitempty"`
	Metadata       json.RawMessage `json:"metadata,omitempty"`
	Precedence     int32           `json:"precedence,omitempty"`
	IdempotencyKey IdempotencyKey  `json:"idempotency_key"`
}

SaveArtifactInput is the transport-neutral artifact creation payload. Project/Kind/Key/Title/Content are validated by ValidateSaveArtifactInput; derived identity, revision numbers and timestamps are store-assigned.

func (SaveArtifactInput) RequestDigest

func (in SaveArtifactInput) RequestDigest() (string, error)

RequestDigest returns the canonical "sha256:<hex>" digest of the creation request payload, excluding the idempotency key itself. Stores persist it in the IdempotencyRecord so retries with the same key replay and key reuse with a different payload conflicts.

type Scope

type Scope string

Scope identifies the resolution scope of an artifact.

const (
	// ScopeWorkspaceDefault is the workspace-wide default scope
	// (project reference empty).
	ScopeWorkspaceDefault Scope = "workspace_default"
	// ScopeProject is an explicit project scope and wins resolution over
	// the workspace default for the same key.
	ScopeProject Scope = "project"
)

func (Scope) Valid

func (s Scope) Valid() bool

Valid reports whether s is an accepted scope.

type ShadowedArtifact

type ShadowedArtifact struct {
	ArtifactID   string `json:"artifact_id"`
	Kind         Kind   `json:"kind"`
	Key          string `json:"key"`
	Revision     int64  `json:"revision"`
	ShadowedByID string `json:"shadowed_by_id"`
}

ShadowedArtifact is a workspace-default artifact overridden by a project artifact for the same (kind,key).

func (ShadowedArtifact) Validate

func (s ShadowedArtifact) Validate() error

Validate checks the shadowed-entry invariants. It does NOT perform the bundle-level crosscheck against the effective set; Protocol.Validate does.

type SoftDeleteInput

type SoftDeleteInput struct {
	ArtifactID  string `json:"artifact_id"`
	IfMatchETag string `json:"if_match_etag"`
	DeletedBy   string `json:"deleted_by"`
	Reason      string `json:"reason"` // required
}

SoftDeleteInput is the only deletion transition in v1: it marks the artifact deleted (excluded from default lists and the effective protocol) while revisions, activations and events are retained indefinitely. There is deliberately NO hard-delete or purge input.

IfMatchETag carries the REQUIRED and ONLY compare-and-swap guard for deletion (REQ-API-003: "delete requires If-Match"): the caller MUST present the artifact's current canonical ETag. There is deliberately NO expected_revision form — deletion is ETag-addressed, so a stale request can never delete state it has not seen. A stale ETag fails with revision_conflict and mutates nothing. DeletedBy (the acting principal) and Reason are mandatory; the store assigns DeletedAt.

func (SoftDeleteInput) Validate

func (in SoftDeleteInput) Validate() error

Validate checks the soft-delete request invariants: the If-Match ETag is required (and is the only accepted precondition), the acting principal is mandatory, and the reason is required and bounded.

type Status

type Status string

Status is the artifact lifecycle state. Deleted is the soft-delete state transition: revisions, activations and events are retained.

const (
	StatusDraft   Status = "draft"
	StatusActive  Status = "active"
	StatusDeleted Status = "deleted"
)

func (Status) Valid

func (st Status) Valid() bool

Valid reports whether st is an accepted status.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL