- Add `content: Option<String>` to SourceRecord with rkyv schema evolution (LegacySourceRecord compat deserializer for backward compatibility) - Add MAX_SOURCE_CONTENT_LEN (1MB) limit with API validation - Strip content from list responses, include in single-source GET - Update Go SDK RegisterSourceRequest with Content field - FCM pipeline extracts PDF text via pdftotext and passes to registration - Dashboard impact panel fetches and displays source content with expand/collapse - Add feed endpoint, dashboard feed panel, and signed assertion support - Update data-structures.md, API docs, and storage docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
281 lines
7.7 KiB
Go
281 lines
7.7 KiB
Go
package steme
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
)
|
|
|
|
// Assertion represents a knowledge graph assertion.
|
|
//
|
|
// This is the primary type for building assertions to submit to StemeDB.
|
|
// Use the fluent builder API with NewAssertion() for ergonomic construction.
|
|
type Assertion struct {
|
|
// The subject entity (e.g., "Tesla_Inc")
|
|
Subject string `json:"subject"`
|
|
|
|
// The predicate/relation (e.g., "has_revenue")
|
|
Predicate string `json:"predicate"`
|
|
|
|
// The object value
|
|
Object ObjectValue `json:"object"`
|
|
|
|
// Confidence score (0.0 to 1.0)
|
|
Confidence float64 `json:"confidence"`
|
|
|
|
// Agent signatures vouching for this assertion
|
|
Signatures []SignatureEntry `json:"signatures"`
|
|
|
|
// Hash of parent assertion (hex-encoded, optional for forks)
|
|
ParentHash *string `json:"parent_hash,omitempty"`
|
|
|
|
// Hash of source evidence (hex-encoded)
|
|
SourceHash string `json:"source_hash"`
|
|
|
|
// Source authority tier (defaults to Expert if not specified)
|
|
SourceClass *SourceClass `json:"source_class,omitempty"`
|
|
|
|
// Perceptual hash for visual anchoring (hex-encoded, optional)
|
|
VisualHash *string `json:"visual_hash,omitempty"`
|
|
|
|
// Epoch ID (hex-encoded, optional)
|
|
Epoch *string `json:"epoch,omitempty"`
|
|
|
|
// Lifecycle stage (defaults to Proposed if not specified)
|
|
Lifecycle *LifecycleStage `json:"lifecycle,omitempty"`
|
|
|
|
// Semantic embedding vector (optional)
|
|
Vector []float32 `json:"vector,omitempty"`
|
|
|
|
// Free-text narrative explaining methodology, limitations, bias, and caveats (optional)
|
|
Narrative *string `json:"narrative,omitempty"`
|
|
}
|
|
|
|
// AssertionBuilder provides a fluent API for building assertions.
|
|
type AssertionBuilder struct {
|
|
assertion Assertion
|
|
}
|
|
|
|
// NewAssertion creates a new assertion builder with required fields.
|
|
//
|
|
// The subject and predicate are required. All other fields can be set
|
|
// via builder methods.
|
|
//
|
|
// Example:
|
|
//
|
|
// assertion := steme.NewAssertion("Tesla_Inc", "has_revenue").
|
|
// WithNumber(96.7).
|
|
// WithConfidence(0.95).
|
|
// WithLifecycle(steme.LifecycleApproved).
|
|
// WithSourceClass(steme.SourceClassClinical)
|
|
func NewAssertion(subject, predicate string) *AssertionBuilder {
|
|
return &AssertionBuilder{
|
|
assertion: Assertion{
|
|
Subject: subject,
|
|
Predicate: predicate,
|
|
Confidence: 1.0, // Default to full confidence
|
|
},
|
|
}
|
|
}
|
|
|
|
// WithText sets the object to a text value.
|
|
func (b *AssertionBuilder) WithText(text string) *AssertionBuilder {
|
|
b.assertion.Object = NewTextValue(text)
|
|
return b
|
|
}
|
|
|
|
// WithNumber sets the object to a numeric value.
|
|
func (b *AssertionBuilder) WithNumber(num float64) *AssertionBuilder {
|
|
b.assertion.Object = NewNumberValue(num)
|
|
return b
|
|
}
|
|
|
|
// WithBoolean sets the object to a boolean value.
|
|
func (b *AssertionBuilder) WithBoolean(val bool) *AssertionBuilder {
|
|
b.assertion.Object = NewBooleanValue(val)
|
|
return b
|
|
}
|
|
|
|
// WithReference sets the object to an entity reference.
|
|
func (b *AssertionBuilder) WithReference(entityID string) *AssertionBuilder {
|
|
b.assertion.Object = NewReferenceValue(entityID)
|
|
return b
|
|
}
|
|
|
|
// WithObject sets the object value directly.
|
|
func (b *AssertionBuilder) WithObject(obj ObjectValue) *AssertionBuilder {
|
|
b.assertion.Object = obj
|
|
return b
|
|
}
|
|
|
|
// WithConfidence sets the confidence score (0.0 to 1.0).
|
|
func (b *AssertionBuilder) WithConfidence(confidence float64) *AssertionBuilder {
|
|
b.assertion.Confidence = confidence
|
|
return b
|
|
}
|
|
|
|
// WithSourceHash sets the source evidence hash (hex-encoded, 32 bytes).
|
|
func (b *AssertionBuilder) WithSourceHash(hashHex string) *AssertionBuilder {
|
|
b.assertion.SourceHash = hashHex
|
|
return b
|
|
}
|
|
|
|
// WithSourceClass sets the source authority tier.
|
|
func (b *AssertionBuilder) WithSourceClass(sc SourceClass) *AssertionBuilder {
|
|
b.assertion.SourceClass = &sc
|
|
return b
|
|
}
|
|
|
|
// WithLifecycle sets the lifecycle stage.
|
|
func (b *AssertionBuilder) WithLifecycle(lifecycle LifecycleStage) *AssertionBuilder {
|
|
b.assertion.Lifecycle = &lifecycle
|
|
return b
|
|
}
|
|
|
|
// WithParentHash sets the parent assertion hash (hex-encoded, 32 bytes).
|
|
//
|
|
// This is used for forking existing assertions.
|
|
func (b *AssertionBuilder) WithParentHash(hashHex string) *AssertionBuilder {
|
|
b.assertion.ParentHash = &hashHex
|
|
return b
|
|
}
|
|
|
|
// WithVisualHash sets the perceptual hash (hex-encoded, 8 bytes).
|
|
//
|
|
// This is used for visual anchoring (e.g., screenshots of tables).
|
|
func (b *AssertionBuilder) WithVisualHash(hashHex string) *AssertionBuilder {
|
|
b.assertion.VisualHash = &hashHex
|
|
return b
|
|
}
|
|
|
|
// WithEpoch sets the epoch ID (hex-encoded, 32 bytes).
|
|
func (b *AssertionBuilder) WithEpoch(epochHex string) *AssertionBuilder {
|
|
b.assertion.Epoch = &epochHex
|
|
return b
|
|
}
|
|
|
|
// WithNarrative sets the free-text narrative (methodology, limitations, caveats).
|
|
func (b *AssertionBuilder) WithNarrative(narrative string) *AssertionBuilder {
|
|
b.assertion.Narrative = &narrative
|
|
return b
|
|
}
|
|
|
|
// WithVector sets the semantic embedding vector.
|
|
func (b *AssertionBuilder) WithVector(vector []float32) *AssertionBuilder {
|
|
b.assertion.Vector = vector
|
|
return b
|
|
}
|
|
|
|
// WithSignatures sets the signature entries directly.
|
|
//
|
|
// This is used if you've pre-computed signatures. Most users should
|
|
// let the Client sign automatically.
|
|
func (b *AssertionBuilder) WithSignatures(sigs []SignatureEntry) *AssertionBuilder {
|
|
b.assertion.Signatures = sigs
|
|
return b
|
|
}
|
|
|
|
// Build returns the constructed Assertion.
|
|
//
|
|
// This does NOT validate the assertion. Validation happens on the server.
|
|
func (b *AssertionBuilder) Build() Assertion {
|
|
return b.assertion
|
|
}
|
|
|
|
// Validate checks the assertion for common errors.
|
|
//
|
|
// Returns an error if:
|
|
// - Confidence is not in [0.0, 1.0]
|
|
// - Object value is not set
|
|
// - SourceHash is empty
|
|
// - Hex-encoded hashes have invalid lengths
|
|
func (a *Assertion) Validate() error {
|
|
if a.Confidence < 0.0 || a.Confidence > 1.0 {
|
|
return &ValidationError{
|
|
Field: "confidence",
|
|
Message: fmt.Sprintf("must be between 0.0 and 1.0, got %f", a.Confidence),
|
|
}
|
|
}
|
|
|
|
if a.Object.Type == "" {
|
|
return &ValidationError{
|
|
Field: "object",
|
|
Message: "object value must be set",
|
|
}
|
|
}
|
|
|
|
if a.SourceHash == "" {
|
|
return &ValidationError{
|
|
Field: "source_hash",
|
|
Message: "source_hash is required",
|
|
}
|
|
}
|
|
|
|
// Validate source_hash is 64 hex chars (32 bytes)
|
|
if len(a.SourceHash) != 64 {
|
|
return &ValidationError{
|
|
Field: "source_hash",
|
|
Message: fmt.Sprintf("must be 64 hex characters (32 bytes), got %d", len(a.SourceHash)),
|
|
}
|
|
}
|
|
|
|
if _, err := hex.DecodeString(a.SourceHash); err != nil {
|
|
return &ValidationError{
|
|
Field: "source_hash",
|
|
Message: fmt.Sprintf("invalid hex encoding: %v", err),
|
|
}
|
|
}
|
|
|
|
// Validate parent_hash if present
|
|
if a.ParentHash != nil {
|
|
if len(*a.ParentHash) != 64 {
|
|
return &ValidationError{
|
|
Field: "parent_hash",
|
|
Message: fmt.Sprintf("must be 64 hex characters (32 bytes), got %d", len(*a.ParentHash)),
|
|
}
|
|
}
|
|
|
|
if _, err := hex.DecodeString(*a.ParentHash); err != nil {
|
|
return &ValidationError{
|
|
Field: "parent_hash",
|
|
Message: fmt.Sprintf("invalid hex encoding: %v", err),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate visual_hash if present (8 bytes = 16 hex chars)
|
|
if a.VisualHash != nil {
|
|
if len(*a.VisualHash) != 16 {
|
|
return &ValidationError{
|
|
Field: "visual_hash",
|
|
Message: fmt.Sprintf("must be 16 hex characters (8 bytes), got %d", len(*a.VisualHash)),
|
|
}
|
|
}
|
|
|
|
if _, err := hex.DecodeString(*a.VisualHash); err != nil {
|
|
return &ValidationError{
|
|
Field: "visual_hash",
|
|
Message: fmt.Sprintf("invalid hex encoding: %v", err),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate epoch if present
|
|
if a.Epoch != nil {
|
|
if len(*a.Epoch) != 64 {
|
|
return &ValidationError{
|
|
Field: "epoch",
|
|
Message: fmt.Sprintf("must be 64 hex characters (32 bytes), got %d", len(*a.Epoch)),
|
|
}
|
|
}
|
|
|
|
if _, err := hex.DecodeString(*a.Epoch); err != nil {
|
|
return &ValidationError{
|
|
Field: "epoch",
|
|
Message: fmt.Sprintf("invalid hex encoding: %v", err),
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|