Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8037e66998 | ||
|
|
29815c49b8 |
@@ -0,0 +1,108 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/world"
|
||||||
|
worlddomain "git.cin.su/m/Assistant/modules/world/domain"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorldRepository struct{ pool *pgxpool.Pool }
|
||||||
|
|
||||||
|
func OpenWorld(ctx context.Context, config ContinuityConfig) (*WorldRepository, error) {
|
||||||
|
pool, err := openPool(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &WorldRepository{pool: pool}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorldRepository) Close() { r.pool.Close() }
|
||||||
|
|
||||||
|
func (r *WorldRepository) SaveKindDefinition(ctx context.Context, value world.WorldObjectKindDefinition) error {
|
||||||
|
if err := worlddomain.ValidateKindDefinition(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var parentOwner, parentKind, parentID string
|
||||||
|
var parentRevision *uint64
|
||||||
|
if value.ParentKindRef != nil {
|
||||||
|
parentOwner, parentKind, parentID = value.ParentKindRef.OwnerNamespace, value.ParentKindRef.SemanticKind, string(value.ParentKindRef.StableID)
|
||||||
|
if value.ParentKindRef.Revision != nil {
|
||||||
|
revision := uint64(*value.ParentKindRef.Revision)
|
||||||
|
parentRevision = &revision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := r.pool.Exec(ctx, `INSERT INTO world.kind_definitions(kind_definition_id,kind_key,revision,display_name,parent_owner_namespace,parent_semantic_kind,parent_stable_id,parent_revision,lifecycle,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT(kind_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.DisplayName, parentOwner, parentKind, parentID, parentRevision, value.Lifecycle, value.DefinitionDigest, value.RecordedAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorldRepository) SaveObject(ctx context.Context, value world.WorldObject) error {
|
||||||
|
if err := worlddomain.ValidateObject(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := r.pool.Exec(ctx, `INSERT INTO world.objects(world_object_id,principal_id,kind_owner_namespace,kind_semantic_kind,kind_stable_id,kind_revision,lifecycle,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(world_object_id) DO NOTHING`, value.ID, value.PrincipalID, value.KindDefinitionRef.OwnerNamespace, value.KindDefinitionRef.SemanticKind, value.KindDefinitionRef.StableID, value.KindRevision, value.Lifecycle, value.CreatedAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorldRepository) SaveAlias(ctx context.Context, value world.WorldAlias) error {
|
||||||
|
if err := worlddomain.ValidateAlias(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := r.pool.Exec(ctx, `INSERT INTO world.aliases(world_alias_id,principal_id,world_object_id,alias,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(world_alias_id) DO NOTHING`, value.ID, value.PrincipalID, value.ObjectID, value.Alias, value.RecordedAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorldRepository) SaveFacetDefinition(ctx context.Context, value world.FacetDefinition) error {
|
||||||
|
if err := worlddomain.ValidateFacetDefinition(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(value.ValueType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encode facet value type: %w", err)
|
||||||
|
}
|
||||||
|
_, err = r.pool.Exec(ctx, `INSERT INTO world.facet_definitions(facet_definition_id,kind_key,revision,facet_key,value_type,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(facet_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.FacetKey, payload, value.DefinitionDigest, value.RecordedAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorldRepository) SaveRelationDefinition(ctx context.Context, value world.RelationDefinition) error {
|
||||||
|
if err := worlddomain.ValidateRelationDefinition(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := r.pool.Exec(ctx, `INSERT INTO world.relation_definitions(relation_definition_id,kind_key,revision,relation_key,target_kind_key,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(relation_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.RelationKey, value.TargetKindKey, value.DefinitionDigest, value.RecordedAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorldRepository) SaveResolution(ctx context.Context, value world.IdentityResolutionDecisionRecord) error {
|
||||||
|
if err := worlddomain.ValidateResolution(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
candidates, err := json.Marshal(value.Candidates)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encode resolution candidates: %w", err)
|
||||||
|
}
|
||||||
|
evidence, err := json.Marshal(value.Evidence)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encode resolution evidence: %w", err)
|
||||||
|
}
|
||||||
|
_, err = r.pool.Exec(ctx, `INSERT INTO world.identity_resolution_decisions(resolution_decision_id,principal_id,candidates,decision,evidence,confidence_millis,decided_at_unix_millis,revision) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(resolution_decision_id) DO NOTHING`, value.ID, value.PrincipalID, candidates, value.Decision, evidence, value.ConfidenceMillis, value.DecidedAt, value.Revision)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorldRepository) SaveLineage(ctx context.Context, value world.WorldLineage) error {
|
||||||
|
if err := worlddomain.ValidateLineage(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sources, err := json.Marshal(value.SourceRefs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encode lineage sources: %w", err)
|
||||||
|
}
|
||||||
|
successors, err := json.Marshal(value.SuccessorRefs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encode lineage successors: %w", err)
|
||||||
|
}
|
||||||
|
_, err = r.pool.Exec(ctx, `INSERT INTO world.lineage(lineage_id,operation,source_refs,successor_refs,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(lineage_id) DO NOTHING`, value.ID, value.Operation, sources, successors, value.RecordedAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import "git.cin.su/m/Assistant/platform/ids"
|
||||||
|
|
||||||
|
// SemanticReference is a typed cross-owner reference; settings remain
|
||||||
|
// Configuration-owned records.
|
||||||
|
type SemanticReference = ids.SemanticRef
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package epistemics
|
||||||
|
|
||||||
|
import "git.cin.su/m/Assistant/platform/ids"
|
||||||
|
|
||||||
|
// SemanticReference identifies evidence-related records without transferring
|
||||||
|
// canonical ownership to Epistemics.
|
||||||
|
type SemanticReference = ids.SemanticRef
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package self
|
||||||
|
|
||||||
|
import "git.cin.su/m/Assistant/platform/ids"
|
||||||
|
|
||||||
|
// SemanticReference lets SELF cite evidence and capabilities without owning
|
||||||
|
// their canonical state.
|
||||||
|
type SemanticReference = ids.SemanticRef
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package situation
|
||||||
|
|
||||||
|
import "git.cin.su/m/Assistant/platform/ids"
|
||||||
|
|
||||||
|
// SemanticReference keeps Situation references transport-neutral while
|
||||||
|
// ContextScope semantics remain owned by Situation.
|
||||||
|
type SemanticReference = ids.SemanticRef
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/identity"
|
||||||
|
"git.cin.su/m/Assistant/platform/clock"
|
||||||
|
"git.cin.su/m/Assistant/platform/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SemanticReference is the transport envelope used by World contracts. It is
|
||||||
|
// a reference only; it does not expose another owner's storage.
|
||||||
|
type SemanticReference = ids.SemanticRef
|
||||||
|
|
||||||
|
type PrincipalRealm string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RealmPersonal PrincipalRealm = "PERSONAL"
|
||||||
|
RealmShared PrincipalRealm = "SHARED"
|
||||||
|
RealmPublic PrincipalRealm = "PUBLIC"
|
||||||
|
RealmSystem PrincipalRealm = "SYSTEM"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SensitivityClass string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SensitivityPublic SensitivityClass = "PUBLIC"
|
||||||
|
SensitivityPrivate SensitivityClass = "PRIVATE"
|
||||||
|
SensitivityRestricted SensitivityClass = "RESTRICTED"
|
||||||
|
SensitivitySecretReference SensitivityClass = "SECRET_REFERENCE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValueTypeKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ValueString ValueTypeKind = "STRING"
|
||||||
|
ValueInteger ValueTypeKind = "INTEGER"
|
||||||
|
ValueDecimal ValueTypeKind = "DECIMAL"
|
||||||
|
ValueBoolean ValueTypeKind = "BOOLEAN"
|
||||||
|
ValueTime ValueTypeKind = "TIME"
|
||||||
|
ValueDuration ValueTypeKind = "DURATION"
|
||||||
|
ValueEnum ValueTypeKind = "ENUM"
|
||||||
|
ValueReference ValueTypeKind = "REFERENCE"
|
||||||
|
ValueBinaryRef ValueTypeKind = "BINARY_REF"
|
||||||
|
ValueList ValueTypeKind = "LIST"
|
||||||
|
ValueStruct ValueTypeKind = "STRUCT"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValueField struct {
|
||||||
|
Name string
|
||||||
|
Type ValueTypeSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueTypeSpec struct {
|
||||||
|
Kind ValueTypeKind
|
||||||
|
Element *ValueTypeSpec
|
||||||
|
Fields []ValueField
|
||||||
|
EnumValues []string
|
||||||
|
ReferenceKind string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorldObjectID ids.StableID
|
||||||
|
type KindDefinitionID ids.StableID
|
||||||
|
type WorldAliasID ids.StableID
|
||||||
|
type FacetDefinitionID ids.StableID
|
||||||
|
type RelationDefinitionID ids.StableID
|
||||||
|
type ResolutionDecisionID ids.StableID
|
||||||
|
type LineageID ids.StableID
|
||||||
|
|
||||||
|
type LifecycleState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LifecycleActive LifecycleState = "ACTIVE"
|
||||||
|
LifecycleMerged LifecycleState = "MERGED"
|
||||||
|
LifecycleSplit LifecycleState = "SPLIT"
|
||||||
|
LifecycleRetired LifecycleState = "RETIRED"
|
||||||
|
LifecycleUnknownIdentity LifecycleState = "UNKNOWN_IDENTITY"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorldObject struct {
|
||||||
|
ID WorldObjectID
|
||||||
|
PrincipalID identity.PrincipalID
|
||||||
|
KindDefinitionRef SemanticReference
|
||||||
|
KindRevision ids.Revision
|
||||||
|
Lifecycle LifecycleState
|
||||||
|
CreatedAt clock.Instant
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorldObjectKindDefinition struct {
|
||||||
|
ID KindDefinitionID
|
||||||
|
KindKey string
|
||||||
|
Revision ids.Revision
|
||||||
|
DisplayName string
|
||||||
|
ParentKindRef *SemanticReference
|
||||||
|
Lifecycle LifecycleState
|
||||||
|
DefinitionDigest string
|
||||||
|
RecordedAt clock.Instant
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorldAlias struct {
|
||||||
|
ID WorldAliasID
|
||||||
|
PrincipalID identity.PrincipalID
|
||||||
|
ObjectID WorldObjectID
|
||||||
|
Alias string
|
||||||
|
RecordedAt clock.Instant
|
||||||
|
}
|
||||||
|
|
||||||
|
type FacetDefinition struct {
|
||||||
|
ID FacetDefinitionID
|
||||||
|
KindKey string
|
||||||
|
Revision ids.Revision
|
||||||
|
FacetKey string
|
||||||
|
ValueType ValueTypeSpec
|
||||||
|
DefinitionDigest string
|
||||||
|
RecordedAt clock.Instant
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelationDefinition struct {
|
||||||
|
ID RelationDefinitionID
|
||||||
|
KindKey string
|
||||||
|
Revision ids.Revision
|
||||||
|
RelationKey string
|
||||||
|
TargetKindKey string
|
||||||
|
DefinitionDigest string
|
||||||
|
RecordedAt clock.Instant
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResolutionDecision string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResolutionSame ResolutionDecision = "SAME"
|
||||||
|
ResolutionDistinct ResolutionDecision = "DISTINCT"
|
||||||
|
ResolutionMerge ResolutionDecision = "MERGE"
|
||||||
|
ResolutionSplit ResolutionDecision = "SPLIT"
|
||||||
|
ResolutionUnresolved ResolutionDecision = "UNRESOLVED"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IdentityResolutionCandidate struct {
|
||||||
|
ObjectRef SemanticReference
|
||||||
|
Evidence []SemanticReference
|
||||||
|
Confidence int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityResolutionDecisionRecord struct {
|
||||||
|
ID ResolutionDecisionID
|
||||||
|
PrincipalID identity.PrincipalID
|
||||||
|
Candidates []IdentityResolutionCandidate
|
||||||
|
Decision ResolutionDecision
|
||||||
|
Evidence []SemanticReference
|
||||||
|
ConfidenceMillis int64
|
||||||
|
DecidedAt clock.Instant
|
||||||
|
Revision ids.Revision
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorldLineage struct {
|
||||||
|
ID LineageID
|
||||||
|
Operation ResolutionDecision
|
||||||
|
SourceRefs []SemanticReference
|
||||||
|
SuccessorRefs []SemanticReference
|
||||||
|
RecordedAt clock.Instant
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WorldObject) Validate() error {
|
||||||
|
if w.ID == "" || !w.KindDefinitionRef.Valid() || !w.KindRevision.Valid() || !validLifecycle(w.Lifecycle) || !w.CreatedAt.Valid() {
|
||||||
|
return fmt.Errorf("world object is incomplete")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k WorldObjectKindDefinition) Validate() error {
|
||||||
|
if k.ID == "" || k.KindKey == "" || !k.Revision.Valid() || k.DisplayName == "" || !validLifecycle(k.Lifecycle) || k.DefinitionDigest == "" || !k.RecordedAt.Valid() {
|
||||||
|
return fmt.Errorf("world kind definition is incomplete")
|
||||||
|
}
|
||||||
|
if k.ParentKindRef != nil && !k.ParentKindRef.Valid() {
|
||||||
|
return fmt.Errorf("world kind parent reference is invalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a WorldAlias) Validate() error {
|
||||||
|
if a.ID == "" || a.ObjectID == "" || a.Alias == "" || !a.RecordedAt.Valid() {
|
||||||
|
return fmt.Errorf("world alias is incomplete")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FacetDefinition) Validate() error {
|
||||||
|
if f.ID == "" || f.KindKey == "" || !f.Revision.Valid() || f.FacetKey == "" || f.DefinitionDigest == "" || !f.RecordedAt.Valid() {
|
||||||
|
return fmt.Errorf("facet definition is incomplete")
|
||||||
|
}
|
||||||
|
return f.ValueType.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RelationDefinition) Validate() error {
|
||||||
|
if r.ID == "" || r.KindKey == "" || !r.Revision.Valid() || r.RelationKey == "" || r.TargetKindKey == "" || r.DefinitionDigest == "" || !r.RecordedAt.Valid() {
|
||||||
|
return fmt.Errorf("relation definition is incomplete")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d IdentityResolutionDecisionRecord) Validate() error {
|
||||||
|
if d.ID == "" || !validDecision(d.Decision) || len(d.Candidates) == 0 || d.ConfidenceMillis < 0 || !d.DecidedAt.Valid() || !d.Revision.Valid() {
|
||||||
|
return fmt.Errorf("identity resolution decision is incomplete")
|
||||||
|
}
|
||||||
|
for _, candidate := range d.Candidates {
|
||||||
|
if !candidate.ObjectRef.Valid() || candidate.Confidence < 0 || candidate.Confidence > 1000 {
|
||||||
|
return fmt.Errorf("identity resolution candidate is invalid")
|
||||||
|
}
|
||||||
|
for _, evidence := range candidate.Evidence {
|
||||||
|
if !evidence.Valid() {
|
||||||
|
return fmt.Errorf("identity resolution evidence reference is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l WorldLineage) Validate() error {
|
||||||
|
if l.ID == "" || (l.Operation != ResolutionMerge && l.Operation != ResolutionSplit) || len(l.SourceRefs) == 0 || len(l.SuccessorRefs) == 0 || !l.RecordedAt.Valid() {
|
||||||
|
return fmt.Errorf("world lineage is incomplete")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validLifecycle(value LifecycleState) bool {
|
||||||
|
return value == LifecycleActive || value == LifecycleMerged || value == LifecycleSplit || value == LifecycleRetired || value == LifecycleUnknownIdentity
|
||||||
|
}
|
||||||
|
|
||||||
|
func validDecision(value ResolutionDecision) bool {
|
||||||
|
return value == ResolutionSame || value == ResolutionDistinct || value == ResolutionMerge || value == ResolutionSplit || value == ResolutionUnresolved
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ValueTypeSpec) Validate() error {
|
||||||
|
if s.Kind == "" {
|
||||||
|
return fmt.Errorf("value type kind is required")
|
||||||
|
}
|
||||||
|
switch s.Kind {
|
||||||
|
case ValueList:
|
||||||
|
if s.Element == nil {
|
||||||
|
return fmt.Errorf("list element type is required")
|
||||||
|
}
|
||||||
|
return s.Element.Validate()
|
||||||
|
case ValueStruct:
|
||||||
|
if len(s.Fields) == 0 {
|
||||||
|
return fmt.Errorf("struct fields are required")
|
||||||
|
}
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, field := range s.Fields {
|
||||||
|
if field.Name == "" {
|
||||||
|
return fmt.Errorf("struct field name is required")
|
||||||
|
}
|
||||||
|
if _, ok := seen[field.Name]; ok {
|
||||||
|
return fmt.Errorf("duplicate struct field %q", field.Name)
|
||||||
|
}
|
||||||
|
seen[field.Name] = struct{}{}
|
||||||
|
if err := field.Type.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("field %s: %w", field.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ValueEnum:
|
||||||
|
if len(s.EnumValues) == 0 {
|
||||||
|
return fmt.Errorf("enum values are required")
|
||||||
|
}
|
||||||
|
case ValueReference:
|
||||||
|
if s.ReferenceKind == "" {
|
||||||
|
return fmt.Errorf("reference kind is required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package world
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestValueTypeSpecValidatesTypedRecursiveShape(t *testing.T) {
|
||||||
|
spec := ValueTypeSpec{Kind: ValueStruct, Fields: []ValueField{{Name: "name", Type: ValueTypeSpec{Kind: ValueString}}, {Name: "refs", Type: ValueTypeSpec{Kind: ValueList, Element: &ValueTypeSpec{Kind: ValueReference, ReferenceKind: "world.object"}}}}}
|
||||||
|
if err := spec.Validate(); err != nil {
|
||||||
|
t.Fatalf("ValueTypeSpec.Validate() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValueTypeSpecRejectsDuplicateFields(t *testing.T) {
|
||||||
|
spec := ValueTypeSpec{Kind: ValueStruct, Fields: []ValueField{{Name: "same", Type: ValueTypeSpec{Kind: ValueString}}, {Name: "same", Type: ValueTypeSpec{Kind: ValueString}}}}
|
||||||
|
if err := spec.Validate(); err == nil {
|
||||||
|
t.Fatal("duplicate fields were accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/platform/clock"
|
||||||
|
"git.cin.su/m/Assistant/platform/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWorldObjectValidationKeepsReferentSeparateFromClaims(t *testing.T) {
|
||||||
|
object := WorldObject{ID: "object-1", KindDefinitionRef: ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "kind", StableID: "kind-1"}, KindRevision: 1, Lifecycle: LifecycleActive, CreatedAt: clock.Instant(1)}
|
||||||
|
if err := object.Validate(); err != nil {
|
||||||
|
t.Fatalf("WorldObject.Validate() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIdentityResolutionRequiresEvidenceBackedCandidates(t *testing.T) {
|
||||||
|
decision := IdentityResolutionDecisionRecord{ID: "decision-1", Decision: ResolutionSame, Candidates: []IdentityResolutionCandidate{{ObjectRef: ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "object", StableID: "object-1"}, Confidence: 900}}, DecidedAt: clock.Instant(1), Revision: 1}
|
||||||
|
if err := decision.Validate(); err != nil {
|
||||||
|
t.Fatalf("IdentityResolutionDecisionRecord.Validate() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE SCHEMA IF NOT EXISTS world;
|
||||||
|
SET ROLE world_role;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS world.kind_definitions (
|
||||||
|
kind_definition_id TEXT PRIMARY KEY,
|
||||||
|
kind_key TEXT NOT NULL,
|
||||||
|
revision BIGINT NOT NULL CHECK (revision > 0),
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
parent_owner_namespace TEXT NOT NULL DEFAULT '',
|
||||||
|
parent_semantic_kind TEXT NOT NULL DEFAULT '',
|
||||||
|
parent_stable_id TEXT NOT NULL DEFAULT '',
|
||||||
|
parent_revision BIGINT,
|
||||||
|
lifecycle TEXT NOT NULL,
|
||||||
|
definition_digest TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
|
||||||
|
UNIQUE (kind_key, revision)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS world.objects (
|
||||||
|
world_object_id TEXT PRIMARY KEY,
|
||||||
|
principal_id TEXT,
|
||||||
|
kind_owner_namespace TEXT NOT NULL,
|
||||||
|
kind_semantic_kind TEXT NOT NULL,
|
||||||
|
kind_stable_id TEXT NOT NULL,
|
||||||
|
kind_revision BIGINT,
|
||||||
|
lifecycle TEXT NOT NULL,
|
||||||
|
created_at_unix_millis BIGINT NOT NULL CHECK (created_at_unix_millis > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS world.aliases (
|
||||||
|
world_alias_id TEXT PRIMARY KEY,
|
||||||
|
principal_id TEXT,
|
||||||
|
world_object_id TEXT NOT NULL,
|
||||||
|
alias TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS world.facet_definitions (
|
||||||
|
facet_definition_id TEXT PRIMARY KEY,
|
||||||
|
kind_key TEXT NOT NULL,
|
||||||
|
revision BIGINT NOT NULL CHECK (revision > 0),
|
||||||
|
facet_key TEXT NOT NULL,
|
||||||
|
value_type JSONB NOT NULL,
|
||||||
|
definition_digest TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
|
||||||
|
UNIQUE (kind_key, facet_key, revision)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS world.relation_definitions (
|
||||||
|
relation_definition_id TEXT PRIMARY KEY,
|
||||||
|
kind_key TEXT NOT NULL,
|
||||||
|
revision BIGINT NOT NULL CHECK (revision > 0),
|
||||||
|
relation_key TEXT NOT NULL,
|
||||||
|
target_kind_key TEXT NOT NULL,
|
||||||
|
definition_digest TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
|
||||||
|
UNIQUE (kind_key, relation_key, revision)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS world.identity_resolution_decisions (
|
||||||
|
resolution_decision_id TEXT PRIMARY KEY,
|
||||||
|
principal_id TEXT,
|
||||||
|
candidates JSONB NOT NULL,
|
||||||
|
decision TEXT NOT NULL,
|
||||||
|
evidence JSONB NOT NULL,
|
||||||
|
confidence_millis BIGINT NOT NULL CHECK (confidence_millis >= 0),
|
||||||
|
decided_at_unix_millis BIGINT NOT NULL CHECK (decided_at_unix_millis > 0),
|
||||||
|
revision BIGINT NOT NULL CHECK (revision > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS world.lineage (
|
||||||
|
lineage_id TEXT PRIMARY KEY,
|
||||||
|
operation TEXT NOT NULL,
|
||||||
|
source_refs JSONB NOT NULL,
|
||||||
|
successor_refs JSONB NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
RESET ROLE;
|
||||||
|
COMMIT;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"task_file": "dev/tasks/GEN2-001-accept-gen2-owner-registry-and-dependency-rules.json",
|
"task_file": "dev/tasks/GEN2-003-add-narrow-world-owner-and-typed-vocabulary.json",
|
||||||
"task_id": "GEN2-001",
|
"task_revision": 1,
|
||||||
"task_revision": 1
|
"task_id": "GEN2-003"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# world
|
||||||
|
|
||||||
|
World owns typed referents, vocabulary and identity-resolution lineage. It
|
||||||
|
does not own observations, claims, beliefs, or factual values about a
|
||||||
|
referent. Those remain Epistemics-owned evidence.
|
||||||
|
|
||||||
|
World domain code does not import adapters, runtime or provider packages and
|
||||||
|
does not write another owner's canonical state.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "git.cin.su/m/Assistant/contracts/world"
|
||||||
|
|
||||||
|
func ValidateObject(value world.WorldObject) error { return value.Validate() }
|
||||||
|
|
||||||
|
func ValidateKindDefinition(value world.WorldObjectKindDefinition) error { return value.Validate() }
|
||||||
|
|
||||||
|
func ValidateAlias(value world.WorldAlias) error { return value.Validate() }
|
||||||
|
|
||||||
|
func ValidateFacetDefinition(value world.FacetDefinition) error { return value.Validate() }
|
||||||
|
|
||||||
|
func ValidateRelationDefinition(value world.RelationDefinition) error { return value.Validate() }
|
||||||
|
|
||||||
|
func ValidateResolution(value world.IdentityResolutionDecisionRecord) error { return value.Validate() }
|
||||||
|
|
||||||
|
func ValidateLineage(value world.WorldLineage) error { return value.Validate() }
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
SaveKindDefinition(context.Context, world.WorldObjectKindDefinition) error
|
||||||
|
SaveObject(context.Context, world.WorldObject) error
|
||||||
|
SaveAlias(context.Context, world.WorldAlias) error
|
||||||
|
SaveFacetDefinition(context.Context, world.FacetDefinition) error
|
||||||
|
SaveRelationDefinition(context.Context, world.RelationDefinition) error
|
||||||
|
SaveResolution(context.Context, world.IdentityResolutionDecisionRecord) error
|
||||||
|
SaveLineage(context.Context, world.WorldLineage) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package clock
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Instant is a UTC point represented as Unix milliseconds.
|
||||||
|
type Instant int64
|
||||||
|
|
||||||
|
func (i Instant) Valid() bool { return i > 0 }
|
||||||
|
|
||||||
|
func NowInstant() Instant { return FromTime(time.Now().UTC()) }
|
||||||
|
|
||||||
|
func FromTime(value time.Time) Instant { return Instant(value.UTC().UnixMilli()) }
|
||||||
|
|
||||||
|
func (i Instant) Time() time.Time { return time.UnixMilli(int64(i)).UTC() }
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package clock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInstantRoundTripUsesUTCUnixMilliseconds(t *testing.T) {
|
||||||
|
want := time.Date(2026, time.August, 29, 12, 34, 56, 789000000, time.FixedZone("local", 3*60*60))
|
||||||
|
got := FromTime(want).Time()
|
||||||
|
if !got.Equal(want.UTC()) || got.Location() != time.UTC {
|
||||||
|
t.Fatalf("instant round trip = %v, want %v UTC", got, want.UTC())
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-5
@@ -6,14 +6,43 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ID is a transport-neutral opaque identifier primitive. Domain packages wrap it
|
// StableID is a transport-neutral opaque identifier primitive. Domain packages
|
||||||
// in strong semantic ID types; do not pass bare ID across semantic boundaries.
|
// wrap it in strong semantic ID types; do not pass bare StableID across
|
||||||
type ID string
|
// semantic boundaries.
|
||||||
|
type StableID string
|
||||||
|
|
||||||
func New() (ID, error) {
|
// ID is retained as a compatibility name for the V1 primitive.
|
||||||
|
type ID = StableID
|
||||||
|
|
||||||
|
// Revision is the optimistic version of a versioned semantic record.
|
||||||
|
type Revision uint64
|
||||||
|
|
||||||
|
const InitialRevision Revision = 1
|
||||||
|
|
||||||
|
func (r Revision) Valid() bool { return r >= InitialRevision }
|
||||||
|
|
||||||
|
// SemanticRef is a technical cross-owner reference envelope. It identifies a
|
||||||
|
// record but does not grant storage ownership or provide generic CRUD.
|
||||||
|
type SemanticRef struct {
|
||||||
|
OwnerNamespace string
|
||||||
|
SemanticKind string
|
||||||
|
StableID StableID
|
||||||
|
Revision *Revision
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r SemanticRef) Valid() bool {
|
||||||
|
return r.OwnerNamespace != "" && r.SemanticKind != "" && r.StableID != "" &&
|
||||||
|
(r.Revision == nil || r.Revision.Valid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStableID() (StableID, error) {
|
||||||
var b [16]byte
|
var b [16]byte
|
||||||
if _, err := rand.Read(b[:]); err != nil {
|
if _, err := rand.Read(b[:]); err != nil {
|
||||||
return "", fmt.Errorf("generate id: %w", err)
|
return "", fmt.Errorf("generate id: %w", err)
|
||||||
}
|
}
|
||||||
return ID(hex.EncodeToString(b[:])), nil
|
return StableID(hex.EncodeToString(b[:])), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() (ID, error) {
|
||||||
|
return NewStableID()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ids
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNewStableIDIsOpaque128BitHex(t *testing.T) {
|
||||||
|
id, err := NewStableID()
|
||||||
|
if err != nil || len(id) != 32 {
|
||||||
|
t.Fatalf("NewStableID() = %q, %v", id, err)
|
||||||
|
}
|
||||||
|
if !(SemanticRef{OwnerNamespace: "world", SemanticKind: "object", StableID: id}.Valid()) {
|
||||||
|
t.Fatal("stable id was not accepted by semantic reference")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSemanticRefRejectsInvalidRevision(t *testing.T) {
|
||||||
|
revision := Revision(0)
|
||||||
|
ref := SemanticRef{OwnerNamespace: "world", SemanticKind: "object", StableID: "id", Revision: &revision}
|
||||||
|
if ref.Valid() {
|
||||||
|
t.Fatal("semantic reference accepted invalid revision")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user