Ctrl K
ComunidadGuías…

internal/artifact/artifact.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Package artifact defines the Cairn Artifact aggregate — the single shared
// unit and aggregate root — and its creation invariants.
//
// Every artifact carries a short opaque public id, a share type, a body
// reference (or, for bundles, a member manifest), metadata, provenance, an
// access policy, an expiry, and an annotation stream. At creation provenance,
// access policy, and expiry are all mandatory. New kinds (image, webhook,
// trajectory) are modeled as share types over this one aggregate, never as a
// competing top-level entity.
//
// Governing: ADR-0001 (Cairn as AI-Native Artifact Store),
// ADR-0007 (Provenance, Link Access, Default Expiry),
// SPEC-0002 REQ "Artifact Aggregate and Invariants"
package artifact

import (
	"time"

	"github.com/stump-wtf/cairn/internal/errs"
	"github.com/stump-wtf/cairn/internal/redact"
	"github.com/stump-wtf/cairn/internal/tracectx"
)

// ShareType classifies an artifact and (via the registry in SPEC-0002 story #9)
// selects its viewer and anchor affordances. Only the values needed by the core
// are enumerated here; the compile-time registry owns the full set.
type ShareType string

const (
	TypeFile       ShareType = "file"       // generic file, non-previewable fallback
	TypeGZ         ShareType = "gz"         // generic gzipped file
	TypeBundle     ShareType = "bundle"     // manifest of ordered members (ADR-0008)
	TypeTrajectory ShareType = "trajectory" // agent run: span tree, no single body (ADR-0009)
	TypeWebhook    ShareType = "webhook"    // live requestbin: capped captured-request stream, no single body (ADR-0010)
)

// Channel is the surface an artifact was created through. It is derived
// server-side from the authenticated surface and is never taken from a client
// claim (ADR-0007 / SPEC-0002 "Channel is server-derived").
type Channel string

const (
	ChannelWeb Channel = "via web"
	ChannelCLI Channel = "via CLI"
	ChannelMCP Channel = "via MCP"
	ChannelAPI Channel = "via API"
)

// Visibility is the link-based access policy. The default is "link":
// you + anyone with the link (ADR-0007).
type Visibility string

const (
	VisibilityLink    Visibility = "link"
	VisibilityPrivate Visibility = "private"
)

// Provenance is the immutable, server-derived record of who created an artifact
// and how. OnBehalfOf names an agent acting for the human ActorID.
//
// CreatedByUserID is the creator's users row; it records who made the
// artifact and confers no permission (SPEC-0023 REQ "Owner Model"). ActorID
// is that user as rendered on the wire, read from the user row, never stored.
type Provenance struct {
	CreatedByUserID string
	ActorID         string
	OnBehalfOf      string
	// Model names the model that produced the artifact, e.g. "claude-opus-5".
	// Empty means the creator did not report one, which is the honest state for
	// a human posting from the CLI — a viewer omits the row rather than showing
	// it blank. It sits on provenance rather than beside it because it answers
	// the same "where did this come from" question as ActorID/OnBehalfOf, is
	// captured once at ingest, and never changes afterwards.
	Model      string
	Channel    Channel
	CapturedAt time.Time
}

// AccessPolicy is the owner + link visibility for an artifact. Exactly one of
// OwnerUserID and OwnerTeamID is set, and the database enforces it too
// (artifacts_one_owner). Ownership checks compare user ids, never a rendered
// actor string.
//
// Governing: ADR-0029, SPEC-0023 REQ "Owner Model".
type AccessPolicy struct {
	OwnerUserID string
	OwnerTeamID string
	Visibility  Visibility
}

// OwnedByUser reports whether userID is the owning user. An empty userID owns
// nothing.
func (p AccessPolicy) OwnedByUser(userID string) bool {
	return userID != "" && p.OwnerUserID == userID
}

// Artifact is the aggregate root. ID is the never-exposed internal primary key;
// PublicID is the opaque base62 handle that appears in every URL. BodySHA256
// references a blob (empty for bundles, whose members are modeled separately).
type Artifact struct {
	ID          int64
	PublicID    string
	ShareType   ShareType
	Title       string
	BodySHA256  string
	Size        int64
	MediaType   string
	Previewable bool
	Provenance  Provenance
	Access      AccessPolicy
	// Tags are client-asserted routing strings, normalized (validated and
	// deduplicated) at create. Deliberately a sibling of Provenance rather than
	// part of it: nothing here is server-derived, so nothing here is
	// trustworthy (ADR-0018).
	Tags []string
	// Relations are the other artifacts this one names, declared at create and
	// immutable afterwards. They are the creator's claim about where the artifact
	// came from; whether a given reader may see each target is decided on read,
	// never here (SPEC-0024 REQ "Relations on Read").
	Relations []Relation
	// Trace is the W3C trace context of the run that produced this artifact,
	// parsed from the create's traceparent. Nil means the create carried no valid
	// one, which is the normal state for a human pasting from a shell: it is
	// absent, never guessed (ADR-0030).
	Trace *tracectx.Context
	// Denormalized annotation rollups, maintained by the annotation core
	// service in the same transaction as every annotation write so the Bin
	// (`💬 2 · 👀 3`) and artifact headers render with no per-row subquery.
	// They are exposed separately, never summed (ADR-0006 "Count aggregation",
	// SPEC-0006 REQ "Count Aggregation").
	ReactionCount int
	CommentCount  int
	// PinCount counts image-region annotations — image-region reactions plus
	// pinned comments (ADR-0006).
	PinCount int
	// Redaction is what the ingest secret scanner did to this artifact's
	// content: a status, a count and counts per rule ID, never a value
	// (SPEC-0017 RD-9). It is the owner's to see; an adapter shows it only when
	// the viewer owns the artifact. The zero value is stored as "unscanned", so
	// a create path the scanner is not wired into never claims to be clean.
	Redaction redact.Summary
	ExpiresAt time.Time
	CreatedAt time.Time
}

// bodyless reports whether this share type carries no single content-addressed
// body: a bundle (its members are modeled separately), a trajectory (its
// content is the span tree), or a webhook (its content is the captured-request
// stream, filled by third parties after creation rather than pushed by its
// owner — ADR-0010). Every other type MUST reference a body blob.
func (a *Artifact) bodyless() bool {
	return a.ShareType == TypeBundle || a.ShareType == TypeTrajectory || a.ShareType == TypeWebhook
}

// Validate enforces the creation invariants: a public id, a share type, a body
// reference (unless the type is bodyless — a bundle or trajectory), and
// mandatory provenance, access policy, and expiry. It returns a validation-coded
// domain error on the first violation.
func (a *Artifact) Validate() error {
	switch {
	case a.PublicID == "":
		return errs.Validationf("artifact: missing public id")
	case a.ShareType == "":
		return errs.Validationf("artifact: missing share type")
	case !a.bodyless() && a.BodySHA256 == "":
		return errs.Validationf("artifact: missing body reference")
	case a.Provenance.Channel == "":
		return errs.Validationf("artifact: provenance channel is required")
	case a.Provenance.ActorID == "":
		return errs.Validationf("artifact: provenance actor is required")
	case a.Provenance.CreatedByUserID == "":
		return errs.Validationf("artifact: provenance creator is required")
	case a.Provenance.CapturedAt.IsZero():
		return errs.Validationf("artifact: provenance capture time is required")
	case (a.Access.OwnerUserID == "") == (a.Access.OwnerTeamID == ""):
		return errs.Validationf("artifact: exactly one owning user or team is required")
	case a.Access.Visibility == "":
		return errs.Validationf("artifact: access visibility is required")
	case a.ExpiresAt.IsZero():
		return errs.Validationf("artifact: expiry is required")
	}
	if err := validateNormalizedTags(a.Tags); err != nil {
		return err
	}
	return validateNormalizedRelations(a.Relations)
}