diff --git a/adapters/postgres/world_repository.go b/adapters/postgres/world_repository.go new file mode 100644 index 0000000..ab48ae2 --- /dev/null +++ b/adapters/postgres/world_repository.go @@ -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 +} diff --git a/contracts/world/primitives.go b/contracts/world/primitives.go index 3d52c26..ed7d54c 100644 --- a/contracts/world/primitives.go +++ b/contracts/world/primitives.go @@ -3,6 +3,8 @@ 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" ) @@ -57,6 +59,177 @@ type ValueTypeSpec struct { 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") diff --git a/contracts/world/types_test.go b/contracts/world/types_test.go new file mode 100644 index 0000000..9e1ce09 --- /dev/null +++ b/contracts/world/types_test.go @@ -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) + } +} diff --git a/database/migrations/046_world_foundation.sql b/database/migrations/046_world_foundation.sql new file mode 100644 index 0000000..b676259 --- /dev/null +++ b/database/migrations/046_world_foundation.sql @@ -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; diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 872a17e..8f5e6f2 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/GEN2-002-introduce-shared-gen2-data-primitives-without-shared-domain-semantics.json", - "task_id": "GEN2-002", - "task_revision": 1 -} + "task_file": "dev/tasks/GEN2-003-add-narrow-world-owner-and-typed-vocabulary.json", + "task_revision": 1, + "task_id": "GEN2-003" +} \ No newline at end of file diff --git a/modules/world/README.arch.md b/modules/world/README.arch.md new file mode 100644 index 0000000..53c8c25 --- /dev/null +++ b/modules/world/README.arch.md @@ -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. diff --git a/modules/world/domain/world.go b/modules/world/domain/world.go new file mode 100644 index 0000000..99159ac --- /dev/null +++ b/modules/world/domain/world.go @@ -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() } diff --git a/modules/world/ports/repository.go b/modules/world/ports/repository.go new file mode 100644 index 0000000..834e4dc --- /dev/null +++ b/modules/world/ports/repository.go @@ -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 +}