diff --git a/contracts/configuration/primitives.go b/contracts/configuration/primitives.go new file mode 100644 index 0000000..e1809b4 --- /dev/null +++ b/contracts/configuration/primitives.go @@ -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 diff --git a/contracts/epistemics/primitives.go b/contracts/epistemics/primitives.go new file mode 100644 index 0000000..097038e --- /dev/null +++ b/contracts/epistemics/primitives.go @@ -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 diff --git a/contracts/self/primitives.go b/contracts/self/primitives.go new file mode 100644 index 0000000..c4c1d58 --- /dev/null +++ b/contracts/self/primitives.go @@ -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 diff --git a/contracts/situation/primitives.go b/contracts/situation/primitives.go new file mode 100644 index 0000000..8885258 --- /dev/null +++ b/contracts/situation/primitives.go @@ -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 diff --git a/contracts/world/primitives.go b/contracts/world/primitives.go new file mode 100644 index 0000000..3d52c26 --- /dev/null +++ b/contracts/world/primitives.go @@ -0,0 +1,97 @@ +package world + +import ( + "fmt" + + "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 +} + +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 +} diff --git a/contracts/world/primitives_test.go b/contracts/world/primitives_test.go new file mode 100644 index 0000000..278eef1 --- /dev/null +++ b/contracts/world/primitives_test.go @@ -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") + } +} diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 74b8ef7..872a17e 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/GEN2-001-accept-gen2-owner-registry-and-dependency-rules.json", - "task_id": "GEN2-001", + "task_file": "dev/tasks/GEN2-002-introduce-shared-gen2-data-primitives-without-shared-domain-semantics.json", + "task_id": "GEN2-002", "task_revision": 1 } diff --git a/platform/clock/instant.go b/platform/clock/instant.go new file mode 100644 index 0000000..7db8330 --- /dev/null +++ b/platform/clock/instant.go @@ -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() } diff --git a/platform/clock/instant_test.go b/platform/clock/instant_test.go new file mode 100644 index 0000000..74a83d8 --- /dev/null +++ b/platform/clock/instant_test.go @@ -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()) + } +} diff --git a/platform/ids/ids.go b/platform/ids/ids.go index d4391ff..8389550 100644 --- a/platform/ids/ids.go +++ b/platform/ids/ids.go @@ -6,14 +6,43 @@ import ( "fmt" ) -// ID is a transport-neutral opaque identifier primitive. Domain packages wrap it -// in strong semantic ID types; do not pass bare ID across semantic boundaries. -type ID string +// StableID is a transport-neutral opaque identifier primitive. Domain packages +// wrap it in strong semantic ID types; do not pass bare StableID across +// 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 if _, err := rand.Read(b[:]); err != nil { 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() } diff --git a/platform/ids/ids_test.go b/platform/ids/ids_test.go new file mode 100644 index 0000000..da59f15 --- /dev/null +++ b/platform/ids/ids_test.go @@ -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") + } +}