merge: GEN2-EVOLVE-001 adaptive retrieval
CI / test (push) Failing after 47s

This commit is contained in:
m
2026-08-30 20:25:40 +03:00
21 changed files with 622 additions and 31 deletions
+43 -9
View File
@@ -101,10 +101,10 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
return nil, fmt.Errorf("invalid experience limit")
}
rows, err := r.pool.Query(ctx, `SELECT
experience_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
verification_evidence,confidence_millis,recorded_at_unix_millis
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
FROM epistemics.experience_records ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $1`, limit)
if err != nil {
return nil, err
@@ -113,10 +113,44 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
var result []epistemics.ExperienceRecord
for rows.Next() {
var value epistemics.ExperienceRecord
if err := rows.Scan(&value.ID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis); err != nil {
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
return nil, err
}
result = append(result, value)
}
return result, rows.Err()
}
// RecentExperienceRecordsForPrincipalAndScope is an owner-local Epistemics
// read. It intentionally applies Principal and ContextScope in SQL before any
// Core-side relevance ranking, so another realm's experience never becomes a
// retrieval candidate.
func (r *EpistemicsRepository) RecentExperienceRecordsForPrincipalAndScope(ctx context.Context, principalID, scopeRef string, limit int) ([]epistemics.ExperienceRecord, error) {
if principalID == "" || limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid scoped experience query")
}
rows, err := r.pool.Query(ctx, `SELECT
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
FROM epistemics.experience_records
WHERE principal_id=$1 AND ($2='' OR context_scope_ref=$2)
ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $3`, principalID, scopeRef, limit)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]epistemics.ExperienceRecord, 0)
for rows.Next() {
var value epistemics.ExperienceRecord
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
return nil, err
}
result = append(result, value)
@@ -205,15 +239,15 @@ func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, i
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
experience_id, principal_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
capability_implementation_ref, capability_usage_observation_ref, capability_gap_ref, skill_ref, expectation_id,
expected_outcome, actual_outcome, outcome_ref, outcome_detail, human_response_ref, correction_utterance_id,
verification_evidence, confidence_millis, recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21) ON CONFLICT DO NOTHING`,
value.ID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
verification_evidence, confidence_millis, recorded_at_unix_millis, context_scope_ref, problem_statement, applicability_conditions, why_succeeded_or_failed)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26) ON CONFLICT DO NOTHING`,
value.ID, value.PrincipalID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
value.CapabilityImplementationRef, value.CapabilityUsageObservationRef, value.CapabilityGapRef, value.SkillRef, value.ExpectationID,
value.ExpectedOutcome, value.ActualOutcome, value.OutcomeRef, value.OutcomeDetail, value.HumanResponseRef, value.CorrectionUtteranceID,
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis)
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis, value.ContextScopeRef, value.ProblemStatement, value.ApplicabilityConditions, value.WhySucceededOrFailed)
return err
}
+47 -3
View File
@@ -16,8 +16,10 @@ import (
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
"git.cin.su/m/Assistant/platform/ids"
)
@@ -33,19 +35,41 @@ type cognitiveInterpretationClient struct {
// compiler receives only already-authorized, typed fragments and applies its
// scope and byte-budget guards before the cognitive gateway sees them.
func compileInteractionContext(projection interactiondomain.ContextProjection, focusProjectID string) (string, error) {
if len(projection.Fragments) == 0 {
return compileInteractionContextWithExperience(projection, focusProjectID, nil, "")
}
func compileInteractionContextWithExperience(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) (string, error) {
if len(projection.Fragments) == 0 && len(experiences) == 0 {
return projection.Text, nil
}
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments))
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments)+len(experiences))
for _, fragment := range projection.Fragments {
candidates = append(candidates, cognitive.ContextCandidate{
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
Relevance: fragment.Relevance,
Lane: "interaction",
Lane: "current-fact",
ContextScopeRef: fragment.ScopeRef,
FreshnessState: "CURRENT",
})
}
if needsHistoricalAnalogy(query) {
for _, fragment := range projection.Historical {
candidates = append(candidates, cognitive.ContextCandidate{
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:historical:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
Relevance: fragment.Relevance - 1, Lane: "historical-analogy", ContextScopeRef: fragment.ScopeRef, FreshnessState: "HISTORICAL_ONLY",
})
}
}
retrieved := epistemicsdomain.RetrieveExperience(experiences, "", focusProjectID, query, 4)
for _, record := range retrieved.Positive {
candidates = append(candidates, experienceContextCandidate(record, "positive-experience", false))
}
for _, record := range retrieved.Negative {
candidates = append(candidates, experienceContextCandidate(record, "negative-experience", true))
}
for _, record := range retrieved.Contradictions {
candidates = append(candidates, experienceContextCandidate(record, "contradiction", true))
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: candidates, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: 2048, ContextScopeRef: focusProjectID, FreshnessRequirement: "conversation-current", RiskClass: "low"})
if err != nil {
return "", err
@@ -57,6 +81,26 @@ func compileInteractionContext(projection interactiondomain.ContextProjection, f
return strings.Join(parts, "\n"), nil
}
func needsHistoricalAnalogy(query string) bool {
for _, marker := range []string{"истор", "раньше", "предыдущ", "старое", "аналог"} {
if strings.Contains(strings.ToLower(query), marker) {
return true
}
}
return false
}
func experienceContextCandidate(record epistemics.ExperienceRecord, lane string, negative bool) cognitive.ContextCandidate {
text := strings.TrimSpace(record.ProblemStatement + ": " + record.OutcomeDetail)
if detail := strings.TrimSpace(record.WhySucceededOrFailed); detail != "" {
text += " (" + detail + ")"
}
if len(record.ApplicabilityConditions) > 0 {
text += " [conditions: " + strings.Join(record.ApplicabilityConditions, ", ") + "]"
}
return cognitive.ContextCandidate{Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("experience:" + string(record.ID)), SourceKind: "epistemics-experience", SourceID: "epistemics:experience-records", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: text}, Relevance: 1, Lane: lane, ContextScopeRef: record.ContextScopeRef, FreshnessState: "CURRENT", Negative: negative}
}
const (
// Household interpretation and response review are intentionally compact
// calls. Keeping their output bounded prevents a loaded local model from
@@ -80,6 +80,18 @@ func TestCompileInteractionContextAppliesTypedScopeBeforeGateway(t *testing.T) {
}
}
func TestCompileInteractionContextUsesHistoricalOnlyWhenRequested(t *testing.T) {
projection := interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{{UtteranceID: "current", Text: "код = NEW", Relevance: 2}}, Historical: []interactiondomain.ContextFragment{{UtteranceID: "old", Text: "код = OLD", Relevance: 1, Historical: true}}}
current, err := compileInteractionContextWithExperience(projection, "", nil, "Какой текущий код?")
if err != nil || strings.Contains(current, "OLD") {
t.Fatalf("current context=%q err=%v", current, err)
}
historical, err := compileInteractionContextWithExperience(projection, "", nil, "Какой старый код был раньше?")
if err != nil || !strings.Contains(historical, "OLD") {
t.Fatalf("historical context=%q err=%v", historical, err)
}
}
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
+5
View File
@@ -113,6 +113,7 @@ func (r learningRecorder) recordReminderResponse(ctx context.Context, input remi
actual, confidence := outcomeForHumanResponse(input.Result.Response)
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceContinuity,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs(
@@ -143,6 +144,7 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceInteraction,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodClarification,
@@ -175,6 +177,7 @@ func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastP
if input.Result.Response.ID != "" {
experience.ContextRefs = contextRefs("continuity:response-candidate:" + string(input.Result.Response.Operation))
}
experience.PrincipalID = string(input.Utterance.PrincipalID)
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok {
r.updateReminderSkill(ctx, saved, capability.UsageSkipped, false)
@@ -196,6 +199,7 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodCapabilityExecution,
@@ -232,6 +236,7 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID),
+5 -1
View File
@@ -595,7 +595,11 @@ func main() {
focusProjectID = string(previousFocus.ProjectID)
}
gen2Context := interactiondomain.BuildScopedGen2ContextForQuery(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, effectiveTaskMessage)
if compiledContext, contextErr := compileInteractionContext(gen2Context, focusProjectID); contextErr != nil {
experienceRecords, experienceErr := epistemicsRepository.RecentExperienceRecordsForPrincipalAndScope(r.Context(), string(session.PrincipalID), focusProjectID, 16)
if experienceErr != nil {
log.Printf("epistemics experience context unavailable: %v", experienceErr)
}
if compiledContext, contextErr := compileInteractionContextWithExperience(gen2Context, focusProjectID, experienceRecords, effectiveTaskMessage); contextErr != nil {
log.Printf("interaction context compilation unavailable: %v", contextErr)
} else {
gen2Context.Text = compiledContext
+44 -10
View File
@@ -36,6 +36,14 @@ type ContextOmission struct {
Reason string
}
// ContextInclusion records the bounded compiler decision without exposing
// source text. It is trace-only metadata and is never user-facing output.
type ContextInclusion struct {
ID ContextExcerptID
Lane string
Reason string
}
type ContextBundle struct {
Excerpts []ContextExcerpt
Omissions []ContextOmission
@@ -45,6 +53,7 @@ type ContextBundle struct {
type ContextAssemblyTrace struct {
Included []ContextExcerptID
Inclusions []ContextInclusion
Excluded []ContextOmission
Coverage string
CompilerVersion string
@@ -67,14 +76,9 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
return ContextBundle{}, ErrInvalidContextBudget
}
candidates := append([]ContextCandidate(nil), input.Candidates...)
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].Relevance != candidates[j].Relevance {
return candidates[i].Relevance > candidates[j].Relevance
}
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
})
bundle := ContextBundle{Excerpts: []ContextExcerpt{}, Omissions: []ContextOmission{}}
seen := make(map[ContextExcerptID]struct{}, len(candidates))
eligible := make([]ContextCandidate, 0, len(candidates))
for _, candidate := range candidates {
excerpt := candidate.Excerpt
if _, ok := seen[excerpt.ID]; ok {
@@ -86,8 +90,7 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "context_scope"})
continue
}
reason := permittedContextReason(excerpt, input.DataPolicy)
if reason != "" {
if reason := permittedContextReason(excerpt, input.DataPolicy); reason != "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
continue
}
@@ -95,14 +98,45 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "empty"})
continue
}
eligible = append(eligible, candidate)
}
candidates = eligible
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].Relevance != candidates[j].Relevance {
return candidates[i].Relevance > candidates[j].Relevance
}
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
})
included := make(map[ContextExcerptID]struct{}, len(candidates))
include := func(candidate ContextCandidate, reason string) bool {
excerpt := candidate.Excerpt
bytes := len([]byte(excerpt.Text))
if bundle.Bytes+bytes > input.MaxBytes {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "budget"})
continue
return false
}
bundle.Excerpts = append(bundle.Excerpts, excerpt)
bundle.Bytes += bytes
bundle.Trace.Included = append(bundle.Trace.Included, excerpt.ID)
bundle.Trace.Inclusions = append(bundle.Trace.Inclusions, ContextInclusion{ID: excerpt.ID, Lane: candidate.Lane, Reason: reason})
included[excerpt.ID] = struct{}{}
return true
}
// Preserve one candidate from each semantic lane before filling by score so
// many similar current facts cannot erase a relevant failure or conflict.
for _, lane := range []string{"current-fact", "positive-experience", "negative-experience", "contradiction", "historical-analogy", "interaction"} {
for _, candidate := range candidates {
if candidate.Lane == lane && include(candidate, "lane_coverage") {
break
}
}
}
for _, candidate := range candidates {
if _, ok := included[candidate.Excerpt.ID]; ok {
continue
}
if !include(candidate, "salience") {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: candidate.Excerpt.ID, Reason: "budget"})
}
}
bundle.Trace.Excluded = append([]ContextOmission(nil), bundle.Omissions...)
bundle.Trace.CompilerVersion = "context-compiler-v2-additive"
@@ -12,3 +12,19 @@ func TestContextCompilerV2TraceAndBudgetRemainDeterministic(t *testing.T) {
t.Fatalf("bundle=%#v err=%v", b, err)
}
}
func TestContextCompilerPreservesNegativeLaneWithinBoundedBudget(t *testing.T) {
b, err := cognitive.CompileContext(cognitive.ContextCompileInput{MaxBytes: 27, DataPolicy: capability.DataPolicyLocalOnly, Candidates: []cognitive.ContextCandidate{
{Excerpt: cognitive.ContextExcerpt{ID: "current", Text: "current-value", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 100},
{Excerpt: cognitive.ContextExcerpt{ID: "failure", Text: "known-failure", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "negative-experience", Relevance: 1, Negative: true},
{Excerpt: cognitive.ContextExcerpt{ID: "crowd", Text: "similar-current", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 99},
}})
if err != nil || len(b.Excerpts) != 2 || b.Excerpts[0].ID != "current" || b.Excerpts[1].ID != "failure" {
t.Fatalf("bundle=%#v err=%v", b, err)
}
// The negative record fits and is retained before a lower-value crowd of
// similar facts can consume the remaining bounded budget.
if len(b.Trace.Inclusions) != 2 || b.Trace.Inclusions[0].Reason != "lane_coverage" || b.Trace.Inclusions[1].Reason != "lane_coverage" {
t.Fatalf("trace=%#v", b.Trace)
}
}
+18
View File
@@ -56,6 +56,24 @@ type FreshnessAssessment struct {
RevalidationAttemptRef string
AssessedAtUnixMillis int64
}
// FreshnessRequirement is supplied by the current task/focus owner. It is an
// evaluation input, not Epistemics-owned task state, so freshness remains
// relative to the decision being made.
type FreshnessRequirement struct {
Mode string
MaximumAgeMillis *int64
RequiredVersionRefs []SemanticReference
}
// WithExactVersionBinding is a typed contract helper for callers that need to
// attach an exact environment/version applicability constraint without taking
// a direct dependency on the World package.
func WithExactVersionBinding(value TemporalApplicability, subject, version SemanticReference) TemporalApplicability {
value.VersionBindings = append(value.VersionBindings, world.VersionBinding{SubjectRef: subject, VersionRef: version, MatchMode: world.VersionExact})
return value
}
type RevalidationRequest struct {
ID string
SourceKnowledgeRef string
+10
View File
@@ -102,6 +102,7 @@ type SystemExpectation struct {
// state or grants authority.
type ExperienceRecord struct {
ID ExperienceRecordID
PrincipalID string
SourceKind ExperienceSourceKind
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
@@ -133,6 +134,15 @@ type ExperienceRecord struct {
WhySucceededOrFailed string
}
// ConsolidationCandidate is a rebuildable Epistemics projection. It preserves
// source ExperienceRecord references and cannot activate a Skill by itself.
type ConsolidationCandidate struct {
EpisodeRef string
ExperienceRefs []ExperienceRecordID
PropositionText string
SkillCandidate bool
}
// ReflectionCandidate records that broader reflection may be warranted.
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
// produce candidates only; it cannot directly rewrite canonical state.
@@ -0,0 +1,17 @@
BEGIN;
SET ROLE epistemics_role;
ALTER TABLE epistemics.experience_records
ADD COLUMN IF NOT EXISTS principal_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS context_scope_ref TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS problem_statement TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS applicability_conditions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
ADD COLUMN IF NOT EXISTS why_succeeded_or_failed TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS experience_records_principal_scope_recorded_idx
ON epistemics.experience_records (principal_id, context_scope_ref, recorded_at_unix_millis DESC);
RESET ROLE;
COMMIT;
+1 -1
View File
@@ -1 +1 @@
{"task_file":"dev/tasks/GEN2-COGNITIVE-002-restore-production-cognitive-route.json","task_id":"GEN2-COGNITIVE-002","task_revision":3}
{"task_file":"dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json","task_id":"GEN2-EVOLVE-001","task_revision":3}
@@ -0,0 +1,78 @@
{
"task_id": "GEN2-EVOLVE-001",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Extend the existing Gen2 retrieval fabric with task-relative freshness, bounded salience, scoped positive/negative experience, contradiction candidates and rebuildable consolidation candidates without creating a Memory owner or retiring V1 compatibility.",
"requirement_ids": ["REQ-GEN2-EVOLVE-001-ADAPTIVE-RETRIEVAL", "REQ-GEN2-EVOLVE-001-EXPERIENCE", "REQ-GEN2-EVOLVE-001-CONSOLIDATION"],
"allowed_change_scope": {
"modules": ["interaction", "epistemics", "core-runtime", "cognitive-context", "production evidence"],
"paths": [
"modules/interaction/domain/gen2_context.go",
"modules/interaction/domain/gen2_context_test.go",
"modules/epistemics/domain/freshness.go",
"modules/epistemics/domain/freshness_test.go",
"modules/epistemics/domain/experience.go",
"modules/epistemics/domain/experience_test.go",
"modules/epistemics/domain/adaptive_retrieval.go",
"modules/epistemics/domain/adaptive_retrieval_test.go",
"modules/epistemics/domain/consolidation.go",
"modules/epistemics/domain/consolidation_test.go",
"contracts/epistemics/freshness.go",
"contracts/epistemics/types.go",
"contracts/cognitive/context_compiler.go",
"contracts/cognitive/context_compiler_v2_test.go",
"adapters/postgres/epistemics_repository.go",
"adapters/postgres/interaction_repository.go",
"cmd/core-runtime/main.go",
"cmd/core-runtime/learning.go",
"cmd/core-runtime/cognitive_interpretation.go",
"cmd/core-runtime/cognitive_interpretation_test.go",
"database/migrations/053_gen2_adaptive_experience_scope.sql",
"development/GEN2_MIGRATION_MAP.md",
"docs/evidence/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", ".gitea/workflows/**", "cmd/supervisor/**", "deploy/**", "GEN2-028", "GEN2-029", "GEN2-030", "PersonalMemory write paths", "Gen3/**"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {
"consume": ["existing Interaction utterance and ProjectReference projection", "existing Epistemics ExperienceRecord and Freshness contracts", "existing Cognitive Context Compiler V2"],
"may_change": ["typed Epistemics retrieval and temporal fields", "typed Context Compiler lane and omission metadata", "no public or cross-owner contract"]
},
"semantic_owner": "Interaction remains canonical owner of human utterances and ProjectReference; Epistemics remains canonical owner of ExperienceRecord and task-relative freshness evidence; Context Compiler remains a rebuildable cognitive projection; Core only composes existing owner reads through established repository boundaries.",
"canonical_state": ["Interaction utterance history and ProjectReference", "Epistemics ExperienceRecord and temporal applicability", "no new Memory canonical state", "no direct cross-owner SQL join or write"],
"persistence_and_migrations": ["add only owner-local Epistemics columns for principal and ContextScope references required to retrieve ExperienceRecord without cross-owner joins", "existing rows remain historical and are not deleted", "migration must be additive and rollback-safe", "migration number 053 is assigned because global migrations 051 and 052 already exist on the task base; no existing migration is changed", "cmd/core-runtime/learning.go is included only to persist the existing authenticated principal into the additive Epistemics metadata written by this task"],
"side_effects_and_privileges": ["no external mutation", "no provider override", "no autonomous Skill or Reflex activation", "no scheduler", "no V1 retirement", "no browser acceptance required"],
"state_transitions": [
"task-relative freshness uses observed/verified/validity/volatility/version/provenance inputs and never turns a stale assessment into permanent deletion",
"current versus historical assertion resolution preserves historical lineage and evaluates applicability before recency",
"ProjectReference and principal filtering precede semantic relevance ranking",
"Epistemics positive and negative ExperienceRecord retrieval is separately lane-classified and preserves failure applicability conditions",
"bounded contradiction candidates are retrieved after the primary candidates without confirmation-only selection",
"Context Compiler receives bounded current-fact, experience, contradiction and historical-analogy lanes with explainable inclusions and omissions",
"consolidation is a deterministic rebuildable projection from raw records to episode/proposition/experience/Skill-candidate evidence; it does not write or activate Skills"
],
"failure_retry_restart": ["bounded retrieval never loops", "missing or old Experience metadata remains historical or excluded rather than treated as current", "rebuild/restart recomputes projections from canonical utterance and Epistemics evidence", "no candidate failure changes stable production traffic"],
"assumptions": ["GEN2-COGNITIVE-002 scoped Interaction retrieval remains the primary assertion reader", "Epistemics owns additive ExperienceRecord metadata", "COMPAT observation and V1 retirement remain deferred by owner", "no browser or production promotion is required for this task"],
"acceptance_scenarios": [
"EVOLVE-001-SCN-01 current versus historical value returns the applicable current value while retaining historical evidence",
"EVOLVE-001-SCN-02 fast-aging and slow-aging inputs receive distinct task-relative freshness assessments",
"EVOLVE-001-SCN-03 Company A and Company B are hard-isolated through typed scope references before ranking",
"EVOLVE-001-SCN-04 relevant memory is selected and irrelevant memory is excluded under a bounded context budget with an explainable omission",
"EVOLVE-001-SCN-05 similar work retrieves both successful and failed applicable ExperienceRecord lanes",
"EVOLVE-001-SCN-06 contradiction retrieval emits a bounded opposing candidate without replacing canonical evidence",
"EVOLVE-001-SCN-07 rebuildable consolidation retains source evidence references and survives repository reload without raw-evidence loss"
],
"required_tests": [
"FAST: targeted go test and go vet for changed interaction, epistemics, cognitive context, core-runtime and postgres packages",
"FAST: devctl preflight, check-diff, independent governance, git diff --check",
"MILESTONE: changed owners plus migration consistency and focused restart/rebuild retrieval check; no browser or FULL suite"
],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
+1 -1
View File
@@ -1641,7 +1641,7 @@ Historical migrations are evidence. Gen2 migration starts at the next available
# 41. New Gen2 schema migration groups
Exact migration numbers are assigned after current repository HEAD is fixed.
Exact migration numbers are assigned after current repository HEAD is fixed. The repository uses one global sequence: `051_identity_principal_realms.sql` and `052_identity_creator_accounts.sql` occupy their numbers. GEN2-EVOLVE-001 therefore adds owner-local Epistemics metadata as additive migration `053_gen2_adaptive_experience_scope.sql`; it does not alter either existing migration.
Recommended groups:
+2
View File
@@ -2,6 +2,8 @@
## Gen2 M6 checkpoint — 2026-08-30
- `GEN2-EVOLVE-001 = MILESTONE_ACCEPTED`: bounded adaptive retrieval adds task-relative freshness, computed salience, positive/negative/contradiction experience lanes and provenance-preserving consolidation candidates. Evidence: `docs/evidence/gen2-evolve-001-milestone-2026-08-30.md`. No V1 path was retired and UI/browser acceptance remains deferred by owner.
- `GEN2-028 = COMPLETED_WITH_EVIDENCE`: V1 compatibility inventory is recorded in `docs/evidence/gen2-028-v1-compatibility-inventory-2026-08-30.json`. Current production callers remain for PersonalMemory fallback, ActiveProjectFocus, truth_bundles, ordinary ENV reads, SkillProfile/FastPath; no path was proven `SAFE_TO_RETIRE`.
- `GEN2-029 = COMPLETED_WITH_SAFE_NOOP`: no V1 runtime path or historical evidence was deleted or disabled. The decision and rollback point are recorded in `docs/evidence/gen2-029-v1-retirement-decision-2026-08-30.json`; `FULL_RETIREMENT` was not applicable because no irreversible retirement was performed.
- `GEN2-030_STATUS = ACCEPTANCE_IN_PROGRESS`: this status is a documentation checkpoint, not a final Gen2 PASS. The final gate must confirm current production and repository evidence; the M6 retirement gap remains that active V1 compatibility callers have not yet been proven unused.
@@ -0,0 +1,36 @@
# GEN2-EVOLVE-001 — milestone evidence
- Task/revision: `GEN2-EVOLVE-001 r3`.
- Base: `07f0f6e3807f0c40971a22f7bc31b844c0635857` (`gen2-integration`); task branch: `gen2/gen2-evolve-001`.
- Migration numbering: global. Existing `051_identity_principal_realms.sql` and `052_identity_creator_accounts.sql` occupied the originally chosen number, so the task-owned additive Epistemics migration was renumbered to `053_gen2_adaptive_experience_scope.sql`. No historical/applied migration was edited.
- Canonical ownership: Interaction remains the canonical owner of utterances and typed `ProjectReference`; Epistemics remains the canonical owner of Experience records and temporal evidence; Context Compiler remains rebuildable. No new Memory owner, cross-owner SQL join, canonical writer, privilege, dependency, provider override or V1 retirement was introduced.
## Milestone checks
Executed once on 2026-08-30:
```text
go test -count=1 ./modules/interaction/domain ./modules/epistemics/domain ./contracts/cognitive ./contracts/epistemics ./adapters/postgres ./cmd/core-runtime
go vet ./modules/interaction/domain ./modules/epistemics/domain ./contracts/cognitive ./contracts/epistemics ./adapters/postgres ./cmd/core-runtime
go run ./cmd/devctl preflight -task dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json
go run ./cmd/devctl check-diff -task dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json
go run ./cmd/devctl/independent
go run ./cmd/devctl check-architecture
git diff --check
```
All commands passed. The focused migration consistency check confirmed a unique global sequence through `053`, an additive owner-local migration, and required `BEGIN`, `SET ROLE epistemics_role`, `principal_id`, `context_scope_ref`, and `COMMIT` markers.
## Acceptance covered
- current versus historical assertion selection preserves historical lineage; an explicit correction precedes timestamp tie-breaking;
- task-relative volatility/freshness uses validity, last verification, maximum age and optional version applicability;
- Principal and typed `ProjectReference` scope filtering occur before relevance/salience ranking;
- positive, negative and contradiction Experience lanes are independently retrieved with applicability conditions retained;
- Context Compiler reserves bounded lane coverage, preserves omission/inclusion reasons in an internal trace, and keeps historical analogies opt-in;
- consolidation candidates retain Epistemics experience references and are candidates only: no Skill is written or activated;
- retrieval projections rebuild from canonical Interaction/Epistemics records; no process-local state is required.
## Deferred / next gap
The existing `IterativeRetrievalRequest` contract is present, but no established runtime continuation callback is available in this packet's scope. Iterative retrieval therefore remains a bounded future wiring task; no parallel runtime contract was invented here. Browser/UI acceptance, compatibility observation and V1 retirement remain deferred by owner decision.
@@ -0,0 +1,158 @@
package domain
import (
"sort"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
type ExperienceRetrieval struct {
Positive []epistemics.ExperienceRecord
Negative []epistemics.ExperienceRecord
Contradictions []epistemics.ExperienceRecord
}
// SalienceInput is transient retrieval policy. It deliberately is not stored
// as a permanent importance score on canonical evidence.
type SalienceInput struct {
Relevance int
FocusMatch bool
GoalOrTaskMatch bool
UserIntentStrength int
Importance int
PreviousUsefulness int
CommitmentRelevant bool
Freshness epistemics.FreshnessAssessmentState
Negative bool
ContextCostBytes int
RecordedAtUnixMillis int64
NowUnixMillis int64
}
// ComputeSalience keeps ranking bounded and explainable. Scope and principal
// filtering happen before this function is ever used.
func ComputeSalience(input SalienceInput) int {
score := input.Relevance*100 + input.Importance + input.PreviousUsefulness + input.UserIntentStrength
if input.FocusMatch {
score += 80
}
if input.GoalOrTaskMatch {
score += 60
}
if input.CommitmentRelevant {
score += 40
}
if input.Negative {
score += 30
}
switch input.Freshness {
case epistemics.FreshnessAssessmentCurrent:
score += 40
case epistemics.FreshnessAssessmentAging, epistemics.FreshnessAssessmentAcceptable:
score += 10
case epistemics.FreshnessAssessmentStale, epistemics.FreshnessAssessmentHistoricalOnly, epistemics.FreshnessAssessmentVersionMismatch:
score -= 80
}
if input.NowUnixMillis > input.RecordedAtUnixMillis && input.NowUnixMillis-input.RecordedAtUnixMillis < 24*60*60*1000 {
score += 20
}
return score - input.ContextCostBytes/32
}
// RetrieveExperience filters principal and scope before text relevance. It is
// a derived view over append-only Epistemics records, not a Memory owner.
func RetrieveExperience(records []epistemics.ExperienceRecord, principalID, scopeRef, query string, limit int) ExperienceRetrieval {
if limit < 1 {
limit = 1
}
queryTokens := retrievalTokens(query)
eligible := make([]epistemics.ExperienceRecord, 0, len(records))
for _, record := range records {
if principalID != "" && record.PrincipalID != principalID {
continue
}
if scopeRef != "" && record.ContextScopeRef != scopeRef {
continue
}
if len(queryTokens) > 0 && overlap(queryTokens, retrievalTokens(record.ProblemStatement+" "+record.OutcomeDetail+" "+record.WhySucceededOrFailed)) == 0 {
continue
}
eligible = append(eligible, record)
}
sort.SliceStable(eligible, func(i, j int) bool {
left := experienceSalience(eligible[i], queryTokens)
right := experienceSalience(eligible[j], queryTokens)
if left != right {
return left > right
}
return eligible[i].RecordedAtUnixMillis > eligible[j].RecordedAtUnixMillis
})
result := ExperienceRetrieval{}
for _, record := range eligible {
if len(result.Positive)+len(result.Negative) >= limit {
break
}
switch record.ActualOutcome {
case epistemics.OutcomeSuccess, epistemics.OutcomePartialSuccess:
result.Positive = append(result.Positive, record)
default:
result.Negative = append(result.Negative, record)
if record.ActualOutcome == epistemics.OutcomeConflict || record.ActualOutcome == epistemics.OutcomeUserCorrected || record.ActualOutcome == epistemics.OutcomeStale {
result.Contradictions = append(result.Contradictions, record)
}
}
}
return result
}
func experienceSalience(record epistemics.ExperienceRecord, queryTokens []string) int {
text := record.ProblemStatement + " " + record.OutcomeDetail + " " + record.WhySucceededOrFailed + " " + strings.Join(record.ApplicabilityConditions, " ")
negative := record.ActualOutcome != epistemics.OutcomeSuccess && record.ActualOutcome != epistemics.OutcomePartialSuccess
return ComputeSalience(SalienceInput{
Relevance: overlap(queryTokens, retrievalTokens(text)),
GoalOrTaskMatch: record.GoalRef != "" || record.TaskRef != "",
Importance: int(record.ConfidenceMillis / 10),
CommitmentRelevant: record.ExpectationID != "",
Negative: negative,
ContextCostBytes: len([]byte(text)),
RecordedAtUnixMillis: record.RecordedAtUnixMillis,
NowUnixMillis: record.RecordedAtUnixMillis,
})
}
func BuildConsolidationCandidates(records []epistemics.ExperienceRecord) []epistemics.ConsolidationCandidate {
byProblem := map[string][]epistemics.ExperienceRecord{}
for _, record := range records {
key := strings.TrimSpace(strings.ToLower(record.ProblemStatement))
if key != "" {
byProblem[key] = append(byProblem[key], record)
}
}
result := make([]epistemics.ConsolidationCandidate, 0, len(byProblem))
for problem, grouped := range byProblem {
refs := make([]epistemics.ExperienceRecordID, 0, len(grouped))
for _, record := range grouped {
refs = append(refs, record.ID)
}
result = append(result, epistemics.ConsolidationCandidate{EpisodeRef: "derived:experience:" + problem, ExperienceRefs: refs, PropositionText: problem, SkillCandidate: len(grouped) > 1})
}
return result
}
func retrievalTokens(value string) []string {
return strings.FieldsFunc(strings.ToLower(value), func(r rune) bool { return r < '0' || r > 'z' && (r < 'а' || r > 'я') })
}
func overlap(left, right []string) int {
seen := map[string]bool{}
for _, v := range left {
seen[v] = true
}
n := 0
for _, v := range right {
if seen[v] {
n++
}
}
return n
}
@@ -0,0 +1,33 @@
package domain
import (
"git.cin.su/m/Assistant/contracts/epistemics"
"testing"
)
func TestRetrieveExperienceFiltersScopeBeforeRelevanceAndKeepsFailures(t *testing.T) {
records := []epistemics.ExperienceRecord{
{ID: "success-a", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "sync catalog", ActualOutcome: epistemics.OutcomeSuccess, RecordedAtUnixMillis: 2},
{ID: "failed-a", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "sync catalog", ActualOutcome: epistemics.OutcomeFailed, OutcomeDetail: "old connector failed", RecordedAtUnixMillis: 1},
{ID: "success-b", PrincipalID: "p", ContextScopeRef: "company-b", ProblemStatement: "sync catalog", ActualOutcome: epistemics.OutcomeSuccess, RecordedAtUnixMillis: 3},
}
got := RetrieveExperience(records, "p", "company-a", "sync catalog", 4)
if len(got.Positive) != 1 || got.Positive[0].ID != "success-a" || len(got.Negative) != 1 || got.Negative[0].ID != "failed-a" {
t.Fatalf("retrieval=%#v", got)
}
}
func TestConsolidationPreservesEvidenceWithoutSkillActivation(t *testing.T) {
got := BuildConsolidationCandidates([]epistemics.ExperienceRecord{{ID: "a", ProblemStatement: "sync catalog"}, {ID: "b", ProblemStatement: "sync catalog"}})
if len(got) != 1 || len(got[0].ExperienceRefs) != 2 || !got[0].SkillCandidate {
t.Fatalf("candidates=%#v", got)
}
}
func TestSalienceIsComputedAndDoesNotPersistImportance(t *testing.T) {
current := ComputeSalience(SalienceInput{Relevance: 1, FocusMatch: true, Freshness: epistemics.FreshnessAssessmentCurrent, ContextCostBytes: 32})
stale := ComputeSalience(SalienceInput{Relevance: 1, Freshness: epistemics.FreshnessAssessmentStale, ContextCostBytes: 32})
if current <= stale {
t.Fatalf("current salience=%d stale salience=%d", current, stale)
}
}
+47 -5
View File
@@ -5,12 +5,27 @@ 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
return AssessFreshnessForRequirement(applicability, epistemics.FreshnessRequirement{Mode: mode, MaximumAgeMillis: maximumAgeMillis}, nowMillis)
}
// AssessFreshnessForRequirement evaluates evidence for the task at hand. It
// never writes a permanent stale marker: the same evidence may be current for
// one task and historical-only for another.
func AssessFreshnessForRequirement(applicability epistemics.TemporalApplicability, requirement epistemics.FreshnessRequirement, nowMillis int64) epistemics.FreshnessAssessment {
basis := applicability.ObservedAtUnixMillis
if applicability.LastVerifiedAtUnixMillis != nil && *applicability.LastVerifiedAtUnixMillis > basis {
basis = *applicability.LastVerifiedAtUnixMillis
}
age := nowMillis - basis
if age < 0 {
age = 0
}
state := epistemics.FreshnessAssessmentCurrent
reason := "observation is within applicability window"
reason := "evidence is within task applicability window"
versionState := "NOT_REQUIRED"
if applicability.ValidFromUnixMillis != nil && nowMillis < *applicability.ValidFromUnixMillis {
state, reason = epistemics.FreshnessAssessmentHistoricalOnly, "valid-from has not started"
}
if applicability.ValidUntilUnixMillis != nil && nowMillis > *applicability.ValidUntilUnixMillis {
state, reason = epistemics.FreshnessAssessmentStale, "valid-until elapsed"
}
@@ -20,11 +35,38 @@ func AssessFreshness(applicability epistemics.TemporalApplicability, mode string
if applicability.Volatility.RevalidateAfterMillis != nil && age > *applicability.Volatility.RevalidateAfterMillis && state == epistemics.FreshnessAssessmentCurrent {
state, reason = epistemics.FreshnessAssessmentAging, "revalidation window elapsed"
}
if maximumAgeMillis != nil && age > *maximumAgeMillis {
if requirement.MaximumAgeMillis != nil && age > *requirement.MaximumAgeMillis {
state, reason = epistemics.FreshnessAssessmentStale, "task maximum age exceeded"
}
if mode == "LIVE_REQUIRED" {
if len(requirement.RequiredVersionRefs) > 0 {
versionState = versionMatchState(applicability, requirement.RequiredVersionRefs)
if versionState == "MISMATCH" {
state, reason = epistemics.FreshnessAssessmentVersionMismatch, "task version does not match evidence applicability"
} else if versionState == "UNKNOWN" && state == epistemics.FreshnessAssessmentCurrent {
state, reason = epistemics.FreshnessAssessmentHistoricalOnly, "task version applicability is unknown"
}
}
if requirement.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}
return epistemics.FreshnessAssessment{SourceKnowledgeRef: "knowledge", TaskRef: "task", State: state, Reason: reason, AgeMillis: age, VersionMatchState: versionState, AssessedAtUnixMillis: nowMillis}
}
func versionMatchState(applicability epistemics.TemporalApplicability, required []epistemics.SemanticReference) string {
if len(applicability.VersionBindings) == 0 {
return "UNKNOWN"
}
for _, target := range required {
matched := false
for _, binding := range applicability.VersionBindings {
if binding.VersionRef == target {
matched = true
break
}
}
if !matched {
return "MISMATCH"
}
}
return "MATCH"
}
@@ -3,6 +3,7 @@ package domain_test
import (
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/modules/epistemics/domain"
"git.cin.su/m/Assistant/platform/ids"
"testing"
)
@@ -15,3 +16,23 @@ func TestFreshnessIsTaskRelative(t *testing.T) {
t.Fatalf("got %v and %v", current.State, historical.State)
}
}
func TestFreshnessUsesVerificationAndVersionForTheCurrentTask(t *testing.T) {
verified := int64(90)
fast := int64(20)
slow := int64(500)
fastEvidence := epistemics.TemporalApplicability{ObservedAtUnixMillis: 1, LastVerifiedAtUnixMillis: &verified, Volatility: epistemics.VolatilitySpec{Mode: epistemics.VolatilityCustom, HardStaleAfterMillis: &fast}}
server := ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "server", StableID: "server-1"}
v2 := ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "version", StableID: "v2"}
v3 := ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "version", StableID: "v3"}
slowEvidence := epistemics.WithExactVersionBinding(epistemics.TemporalApplicability{ObservedAtUnixMillis: 1, LastVerifiedAtUnixMillis: &verified, Volatility: epistemics.VolatilitySpec{Mode: epistemics.VolatilityCustom, HardStaleAfterMillis: &slow}}, server, v2)
if got := domain.AssessFreshnessForRequirement(fastEvidence, epistemics.FreshnessRequirement{}, 120); got.State != epistemics.FreshnessAssessmentStale {
t.Fatalf("fast-changing evidence=%#v", got)
}
if got := domain.AssessFreshnessForRequirement(slowEvidence, epistemics.FreshnessRequirement{RequiredVersionRefs: []epistemics.SemanticReference{v2}}, 120); got.State != epistemics.FreshnessAssessmentCurrent || got.VersionMatchState != "MATCH" {
t.Fatalf("slow version-matched evidence=%#v", got)
}
if got := domain.AssessFreshnessForRequirement(slowEvidence, epistemics.FreshnessRequirement{RequiredVersionRefs: []epistemics.SemanticReference{v3}}, 120); got.State != epistemics.FreshnessAssessmentVersionMismatch {
t.Fatalf("version mismatch=%#v", got)
}
}
+16 -1
View File
@@ -76,6 +76,7 @@ type assertionCandidate struct {
fragment ContextFragment
subject string
observed int64
corrects interaction.HumanUtteranceID
}
func explicitAssertionCandidates(history []interaction.HumanUtterance, projectRefs map[interaction.HumanUtteranceID][]interaction.ProjectReference, focusProjectID, query string) []assertionCandidate {
@@ -111,6 +112,7 @@ func explicitAssertionCandidates(history []interaction.HumanUtterance, projectRe
fragment: ContextFragment{UtteranceID: utterance.ID, Text: subject + " = " + value, ScopeRef: scopeRef, Relevance: relevance},
subject: normalizedContextSubject(subject),
observed: utterance.ReceivedAtUnixMillis,
corrects: utterance.CorrectsUtteranceID,
})
}
return result
@@ -120,7 +122,7 @@ func selectCurrentAssertions(candidates []assertionCandidate) ([]ContextFragment
latest := make(map[string]assertionCandidate)
for _, candidate := range candidates {
current, exists := latest[candidate.subject]
if !exists || candidate.observed > current.observed || candidate.observed == current.observed && candidate.fragment.UtteranceID > current.fragment.UtteranceID {
if !exists || supersedesAssertion(candidate, current) {
latest[candidate.subject] = candidate
}
}
@@ -144,6 +146,19 @@ func selectCurrentAssertions(candidates []assertionCandidate) ([]ContextFragment
return current, historical
}
// supersedesAssertion first respects an explicit correction relation, then
// uses observation time only as a deterministic tie-breaker. Applicability is
// already established by principal and typed Scope filtering before this step.
func supersedesAssertion(candidate, current assertionCandidate) bool {
if candidate.corrects == current.fragment.UtteranceID {
return true
}
if current.corrects == candidate.fragment.UtteranceID {
return false
}
return candidate.observed > current.observed || candidate.observed == current.observed && candidate.fragment.UtteranceID > current.fragment.UtteranceID
}
func contextProjectionFromAssertions(current, historical []ContextFragment) ContextProjection {
selected := make([]ContextFragment, 0, len(current))
text := make([]string, 0, len(current))
@@ -60,6 +60,18 @@ func TestGen2ContextKeepsHistoricalAssertionOutOfCurrentProjection(t *testing.T)
}
}
func TestGen2ContextCorrectionSupersedesBeforeTimestampTieBreaker(t *testing.T) {
history := []interaction.HumanUtterance{
{ID: "observed-later", PrincipalID: "principal-1", Content: "контрольный код = LATER", ReceivedAtUnixMillis: 9},
{ID: "original", PrincipalID: "principal-1", Content: "контрольный код = ORIGINAL", ReceivedAtUnixMillis: 1},
{ID: "correction", PrincipalID: "principal-1", Content: "контрольный код = CORRECTED", ReceivedAtUnixMillis: 2, CorrectsUtteranceID: "observed-later"},
}
got := domain.BuildScopedGen2ContextForQuery(history, nil, "principal-1", "", "Какой контрольный код?")
if !strings.Contains(got.Text, "CORRECTED") || strings.Contains(got.Text, "LATER") {
t.Fatalf("explicit correction did not supersede: %#v", got)
}
}
func TestGen2ContextHardFiltersProjectReferencesBeforeRanking(t *testing.T) {
history := []interaction.HumanUtterance{
{ID: "a", PrincipalID: "principal-1", Content: "контрольный код = ALPHA", ReceivedAtUnixMillis: 1},