Compare commits

..
Author SHA1 Message Date
m 0acbe53fad GEN2-009: add rebuildable retrieval projections
CI / test (push) Successful in 50s
2026-08-29 02:38:49 +03:00
m e634bb946a GEN2-008: promote experience and episode projections
CI / test (push) Successful in 55s
2026-08-29 02:37:28 +03:00
m c7ad996cae GEN2-007: add task-relative freshness assessment
CI / test (push) Successful in 52s
2026-08-29 02:35:54 +03:00
m a065382512 GEN2-006: normalize epistemic propositions and assessments
CI / test (push) Successful in 51s
2026-08-29 02:33:09 +03:00
13 changed files with 442 additions and 11 deletions
+90
View File
@@ -0,0 +1,90 @@
package epistemics
import (
"fmt"
"git.cin.su/m/Assistant/contracts/world"
)
type VolatilityMode string
const (
VolatilityImmutable VolatilityMode = "IMMUTABLE"
VolatilityVersionBound VolatilityMode = "VERSION_BOUND"
VolatilityVerySlow VolatilityMode = "VERY_SLOW"
VolatilitySlow VolatilityMode = "SLOW"
VolatilityMedium VolatilityMode = "MEDIUM"
VolatilityFast VolatilityMode = "FAST"
VolatilityEphemeral VolatilityMode = "EPHEMERAL"
VolatilityCustom VolatilityMode = "CUSTOM"
)
type TemporalApplicability struct {
ObservedAtUnixMillis int64
ValidFromUnixMillis *int64
ValidUntilUnixMillis *int64
LastVerifiedAtUnixMillis *int64
Volatility VolatilitySpec
VersionBindings []world.VersionBinding
}
type VolatilitySpec struct {
Mode VolatilityMode
ExpectedChangeIntervalMillis *int64
RevalidateAfterMillis *int64
HardStaleAfterMillis *int64
InvalidateOnVersionChange bool
RevalidationPolicyRef string
}
type FreshnessAssessmentState string
const (
FreshnessAssessmentCurrent FreshnessAssessmentState = "CURRENT"
FreshnessAssessmentAcceptable FreshnessAssessmentState = "ACCEPTABLE"
FreshnessAssessmentAging FreshnessAssessmentState = "AGING"
FreshnessAssessmentStale FreshnessAssessmentState = "STALE"
FreshnessAssessmentVersionMismatch FreshnessAssessmentState = "VERSION_MISMATCH"
FreshnessAssessmentHistoricalOnly FreshnessAssessmentState = "HISTORICAL_ONLY"
FreshnessAssessmentUnknown FreshnessAssessmentState = "UNKNOWN"
)
type FreshnessAssessment struct {
SourceKnowledgeRef string
TaskRef string
State FreshnessAssessmentState
Reason string
AgeMillis int64
VersionMatchState string
RevalidationAttemptRef string
AssessedAtUnixMillis int64
}
type RevalidationRequest struct {
ID string
SourceKnowledgeRef string
TaskRef string
RequiredVersionRefs []world.SemanticReference
RequestedAtUnixMillis int64
}
type RevalidationResponse struct {
RequestID string
Assessment FreshnessAssessment
NewEvidenceRefs []EvidenceRef
CompletedAtUnixMillis int64
}
func (v VolatilitySpec) Validate() error {
if v.Mode == "" {
return fmt.Errorf("volatility mode is required")
}
return nil
}
func (t TemporalApplicability) Validate() error {
if t.ObservedAtUnixMillis < 1 {
return fmt.Errorf("observed time is required")
}
return t.Volatility.Validate()
}
func (a FreshnessAssessment) Validate() error {
if a.SourceKnowledgeRef == "" || a.TaskRef == "" || a.State == "" || a.AgeMillis < 0 || a.AssessedAtUnixMillis < 1 {
return fmt.Errorf("freshness assessment is incomplete")
}
return nil
}
+20 -10
View File
@@ -1,5 +1,7 @@
package epistemics
import "git.cin.su/m/Assistant/platform/ids"
type BeliefState string
const (
@@ -9,19 +11,27 @@ const (
)
type Observation struct {
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
PrincipalID string
ContextScopeRef string
ObservationKind string
Payload string
ExtractorRef string
}
type Claim struct {
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
PropositionID ids.ID
RevisedFrom *ClaimID
RecordedAtUnixMillis int64
}
type Belief struct {
+48
View File
@@ -24,6 +24,8 @@ type HelpMethodKind string
type OutcomeClass string
type ReflectionTriggerKind string
type ReflectionCandidateState string
type PropositionKind string
type BeliefAssessmentState string
const (
VerificationUnverified VerificationState = "UNVERIFIED"
@@ -73,6 +75,12 @@ const (
ReflectionCandidateEvaluating ReflectionCandidateState = "EVALUATING"
ReflectionCandidateSuperseded ReflectionCandidateState = "SUPERSEDED"
ReflectionCandidateResolved ReflectionCandidateState = "RESOLVED"
PropositionWorldFacet PropositionKind = "WORLD_FACET"
PropositionWorldRelation PropositionKind = "WORLD_RELATION"
PropositionOwnerDefined PropositionKind = "OWNER_DEFINED"
PropositionTextual PropositionKind = "TEXTUAL"
BeliefAssessmentCurrent BeliefAssessmentState = "CURRENT"
BeliefAssessmentSuperseded BeliefAssessmentState = "SUPERSEDED"
)
// SystemExpectation records what Domovoy expected a meaningful help path to
@@ -114,6 +122,15 @@ type ExperienceRecord struct {
VerificationEvidence []EvidenceRef
ConfidenceMillis int64
RecordedAtUnixMillis int64
ProblemStatement string
AttemptRef string
WhyAttempted string
ApplicabilityConditions []string
ContextScopeRef string
EpisodeRef string
GoalRef string
TaskRef string
WhySucceededOrFailed string
}
// ReflectionCandidate records that broader reflection may be warranted.
@@ -134,3 +151,34 @@ type ReflectionCandidate struct {
ClaimedBy string
ClaimExpiresAtUnixMillis int64
}
type Proposition struct {
ID ids.ID
Kind PropositionKind
SubjectRef string
PredicateRef string
ObjectValue string
ObjectRef string
Qualifiers []string
TextFallback string
}
type BeliefAssessment struct {
ID ids.ID
ClaimID ClaimID
Revision ids.Revision
State BeliefAssessmentState
ConfidenceMillis int64
EvidenceRefs []EvidenceRef
AssessedAtUnixMillis int64
}
type EpisodeID ids.ID
type Episode struct {
ID EpisodeID
PrincipalID string
ExperienceRefs []ExperienceRecordID
Summary string
Outcome OutcomeClass
RecordedAtUnixMillis int64
}
+28
View File
@@ -0,0 +1,28 @@
package work
import "git.cin.su/m/Assistant/contracts/world"
type FreshnessRequirementMode string
type FreshnessFallbackPolicy string
const (
FreshnessAny FreshnessRequirementMode = "ANY"
FreshnessPreferCurrent FreshnessRequirementMode = "PREFER_CURRENT"
FreshnessRequireCurrent FreshnessRequirementMode = "REQUIRE_CURRENT"
FreshnessRequireWithinAge FreshnessRequirementMode = "REQUIRE_WITHIN_AGE"
FreshnessRequireVersionMatch FreshnessRequirementMode = "REQUIRE_VERSION_MATCH"
FreshnessLiveRequired FreshnessRequirementMode = "LIVE_REQUIRED"
FallbackAllowStaleDisclosure FreshnessFallbackPolicy = "ALLOW_STALE_WITH_DISCLOSURE"
FallbackHypothesisOnly FreshnessFallbackPolicy = "USE_AS_HYPOTHESIS_ONLY"
FallbackWaitForSource FreshnessFallbackPolicy = "WAIT_FOR_SOURCE"
FallbackAskOwner FreshnessFallbackPolicy = "ASK_OWNER"
FallbackStopUnknown FreshnessFallbackPolicy = "STOP_WITH_UNKNOWN"
)
type TaskFreshnessRequirement struct {
Mode FreshnessRequirementMode
MaximumAgeMillis *int64
RequiredVersionRefs []world.SemanticReference
RevalidationRequired bool
FallbackPolicy FreshnessFallbackPolicy
}
+1 -1
View File
@@ -1 +1 @@
{"task_file":"dev/tasks/GEN2-005-world-identity-resolution-and-version-environment-bindings.json","task_id":"GEN2-005","task_revision":1}
{"task_file":"dev/tasks/GEN2-009-build-memory-retrieval-fabric-as-rebuildable-projections.json","task_id":"GEN2-009","task_revision":1}
+29
View File
@@ -0,0 +1,29 @@
package domain
import (
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/platform/ids"
"strings"
)
func BuildEpisode(experiences []epistemics.ExperienceRecord, summary string, at int64) (epistemics.Episode, error) {
if len(experiences) == 0 || strings.TrimSpace(summary) == "" || at < 1 {
return epistemics.Episode{}, ErrInvalidExperience
}
refs := make([]epistemics.ExperienceRecordID, 0, len(experiences))
outcome := epistemics.OutcomeUnknown
for _, e := range experiences {
if _, err := NewExperienceRecord(e); err != nil {
return epistemics.Episode{}, err
}
refs = append(refs, e.ID)
if e.ActualOutcome != epistemics.OutcomeSuccess {
outcome = e.ActualOutcome
}
}
id, err := ids.New()
if err != nil {
return epistemics.Episode{}, err
}
return epistemics.Episode{ID: epistemics.EpisodeID(id), ExperienceRefs: refs, Summary: summary, Outcome: outcome, RecordedAtUnixMillis: at}, nil
}
+15
View File
@@ -0,0 +1,15 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/modules/epistemics/domain"
"testing"
)
func TestEpisodeIsRebuildableFromExperience(t *testing.T) {
e := epistemics.ExperienceRecord{ID: "e", SourceKind: epistemics.ExperienceSourceInteraction, OriginalIntentID: "i", SourceUtteranceID: "u", HelpMethod: epistemics.HelpMethodFallback, ExpectedOutcome: epistemics.OutcomeSuccess, ActualOutcome: epistemics.OutcomeFailed, RecordedAtUnixMillis: 1}
ep, err := domain.BuildEpisode([]epistemics.ExperienceRecord{e}, "failed attempt", 1)
if err != nil || len(ep.ExperienceRefs) != 1 || ep.Outcome != epistemics.OutcomeFailed {
t.Fatalf("episode=%#v err=%v", ep, err)
}
}
+30
View File
@@ -0,0 +1,30 @@
package domain
import "git.cin.su/m/Assistant/contracts/epistemics"
// AssessFreshness receives the Work-owned requirement as primitive policy
// inputs, keeping the Epistemics owner independent of Work package types.
func AssessFreshness(applicability epistemics.TemporalApplicability, mode string, maximumAgeMillis *int64, nowMillis int64) epistemics.FreshnessAssessment {
age := nowMillis - applicability.ObservedAtUnixMillis
if age < 0 {
age = 0
}
state := epistemics.FreshnessAssessmentCurrent
reason := "observation is within applicability window"
if applicability.ValidUntilUnixMillis != nil && nowMillis > *applicability.ValidUntilUnixMillis {
state, reason = epistemics.FreshnessAssessmentStale, "valid-until elapsed"
}
if applicability.Volatility.HardStaleAfterMillis != nil && age > *applicability.Volatility.HardStaleAfterMillis {
state, reason = epistemics.FreshnessAssessmentStale, "hard volatility window elapsed"
}
if applicability.Volatility.RevalidateAfterMillis != nil && age > *applicability.Volatility.RevalidateAfterMillis && state == epistemics.FreshnessAssessmentCurrent {
state, reason = epistemics.FreshnessAssessmentAging, "revalidation window elapsed"
}
if maximumAgeMillis != nil && age > *maximumAgeMillis {
state, reason = epistemics.FreshnessAssessmentStale, "task maximum age exceeded"
}
if mode == "LIVE_REQUIRED" {
state, reason = epistemics.FreshnessAssessmentHistoricalOnly, "live source required"
}
return epistemics.FreshnessAssessment{SourceKnowledgeRef: "knowledge", TaskRef: "task", State: state, Reason: reason, AgeMillis: age, AssessedAtUnixMillis: nowMillis}
}
@@ -0,0 +1,17 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/modules/epistemics/domain"
"testing"
)
func TestFreshnessIsTaskRelative(t *testing.T) {
hard := int64(100)
app := epistemics.TemporalApplicability{ObservedAtUnixMillis: 1, Volatility: epistemics.VolatilitySpec{Mode: epistemics.VolatilityVersionBound, HardStaleAfterMillis: &hard}}
current := domain.AssessFreshness(app, "ANY", nil, 50)
historical := domain.AssessFreshness(app, "LIVE_REQUIRED", nil, 50)
if current.State != epistemics.FreshnessAssessmentCurrent || historical.State != epistemics.FreshnessAssessmentHistoricalOnly {
t.Fatalf("got %v and %v", current.State, historical.State)
}
}
+36
View File
@@ -0,0 +1,36 @@
package domain
import (
"errors"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/platform/ids"
"strings"
)
var ErrInvalidKnowledge = errors.New("invalid epistemic knowledge record")
func ValidateProposition(value epistemics.Proposition) error {
if value.ID == "" || value.Kind == "" || strings.TrimSpace(value.SubjectRef) == "" || (value.ObjectValue == "" && value.ObjectRef == "" && value.TextFallback == "") {
return ErrInvalidKnowledge
}
return nil
}
func ValidateBeliefAssessment(value epistemics.BeliefAssessment) error {
if value.ID == "" || value.ClaimID == "" || !value.Revision.Valid() || value.State == "" || value.ConfidenceMillis < 0 || value.ConfidenceMillis > 1000 || value.AssessedAtUnixMillis < 1 {
return ErrInvalidKnowledge
}
return nil
}
func NewBeliefAssessment(claim epistemics.ClaimID, confidence int64, state epistemics.BeliefAssessmentState, evidence []epistemics.EvidenceRef, at int64) (epistemics.BeliefAssessment, error) {
id, err := ids.New()
if err != nil {
return epistemics.BeliefAssessment{}, err
}
value := epistemics.BeliefAssessment{ID: id, ClaimID: claim, Revision: ids.InitialRevision, State: state, ConfidenceMillis: confidence, EvidenceRefs: append([]epistemics.EvidenceRef(nil), evidence...), AssessedAtUnixMillis: at}
if err := ValidateBeliefAssessment(value); err != nil {
return epistemics.BeliefAssessment{}, err
}
return value, nil
}
@@ -0,0 +1,22 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/modules/epistemics/domain"
"testing"
)
func TestKnowledgeV2SeparatesPropositionAndReassessableBelief(t *testing.T) {
p := epistemics.Proposition{ID: "p", Kind: epistemics.PropositionWorldFacet, SubjectRef: "world:database:a", ObjectValue: "1.6.28"}
if err := domain.ValidateProposition(p); err != nil {
t.Fatal(err)
}
a, err := domain.NewBeliefAssessment("claim:a", 800, epistemics.BeliefAssessmentCurrent, []epistemics.EvidenceRef{"observation:a"}, 1)
if err != nil {
t.Fatal(err)
}
a.Revision++
if err := domain.ValidateBeliefAssessment(a); err != nil {
t.Fatal(err)
}
}
+92
View File
@@ -0,0 +1,92 @@
package retrieval
import "sort"
type Lane string
const (
LaneSemantic Lane = "SEMANTIC"
LaneEntity Lane = "ENTITY"
LaneTemporal Lane = "TEMPORAL"
LaneGoal Lane = "GOAL"
LanePositiveExperience Lane = "POSITIVE_EXPERIENCE"
LaneNegativeExperience Lane = "NEGATIVE_EXPERIENCE"
LaneContradiction Lane = "CONTRADICTION"
LaneSkill Lane = "SKILL"
)
// RetrievalDocument is disposable read-side projection data. SourceRef and
// Owner preserve provenance; this type never becomes canonical meaning.
type RetrievalDocument struct {
ID string
SourceRef string
Owner string
PrincipalID string
ContextScopeRef string
VersionRef string
Lane Lane
Text string
Relevance int
Freshness int
Confidence int
Salience int
Cost int
}
type Query struct {
PrincipalID string
ContextScopeRef string
VersionRef string
Lanes []Lane
Limit int
}
func FilterAndRank(documents []RetrievalDocument, query Query) []RetrievalDocument {
filtered := make([]RetrievalDocument, 0, len(documents))
for _, doc := range documents {
if doc.PrincipalID != query.PrincipalID || (query.ContextScopeRef != "" && doc.ContextScopeRef != query.ContextScopeRef) || (query.VersionRef != "" && doc.VersionRef != query.VersionRef) || !containsLane(query.Lanes, doc.Lane) {
continue
}
filtered = append(filtered, doc)
}
sort.SliceStable(filtered, func(i, j int) bool {
left := filtered[i].Relevance + filtered[i].Freshness + filtered[i].Confidence + filtered[i].Salience - filtered[i].Cost
right := filtered[j].Relevance + filtered[j].Freshness + filtered[j].Confidence + filtered[j].Salience - filtered[j].Cost
if left != right {
return left > right
}
return filtered[i].ID < filtered[j].ID
})
if query.Limit > 0 && len(filtered) > query.Limit {
filtered = filtered[:query.Limit]
}
return filtered
}
func containsLane(lanes []Lane, lane Lane) bool {
if len(lanes) == 0 {
return true
}
for _, v := range lanes {
if v == lane {
return true
}
}
return false
}
type ProjectionBuilder interface {
Build() ([]RetrievalDocument, error)
}
func Rebuild(builders ...ProjectionBuilder) ([]RetrievalDocument, error) {
var result []RetrievalDocument
for _, builder := range builders {
docs, err := builder.Build()
if err != nil {
return nil, err
}
result = append(result, docs...)
}
return result, nil
}
+14
View File
@@ -0,0 +1,14 @@
package retrieval_test
import (
"git.cin.su/m/Assistant/runtime/retrieval"
"testing"
)
func TestScopeAndVersionFilterBeforeRanking(t *testing.T) {
docs := []retrieval.RetrievalDocument{{ID: "wrong-company", PrincipalID: "a", ContextScopeRef: "b", VersionRef: "v", Lane: retrieval.LaneSemantic, Relevance: 100}, {ID: "right", PrincipalID: "a", ContextScopeRef: "a", VersionRef: "v", Lane: retrieval.LaneNegativeExperience, Relevance: 1}, {ID: "wrong-version", PrincipalID: "a", ContextScopeRef: "a", VersionRef: "old", Lane: retrieval.LaneSemantic, Relevance: 100}}
got := retrieval.FilterAndRank(docs, retrieval.Query{PrincipalID: "a", ContextScopeRef: "a", VersionRef: "v", Lanes: []retrieval.Lane{retrieval.LaneSemantic, retrieval.LaneNegativeExperience}, Limit: 10})
if len(got) != 1 || got[0].ID != "right" {
t.Fatalf("got %#v", got)
}
}