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
10 changed files with 335 additions and 1 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
}
+19
View File
@@ -122,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.
@@ -163,3 +172,13 @@ type BeliefAssessment struct {
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-006-normalize-epistemics-evidence-propositions-claims-and-belief-assessments.json","task_id":"GEN2-006","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)
}
}
+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)
}
}