From 82b89a37d387beaf70b3cf37a40f48f3da9ff69e Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 30 Aug 2026 22:26:09 +0300 Subject: [PATCH] feat(self): add bounded improvement learning loop --- adapters/postgres/self_repository.go | 69 +++++++ contracts/self/benchmark.go | 12 ++ contracts/self/types.go | 170 +++++++++++++++- ..._self_weakness_diagnosis_and_benchmark.sql | 74 +++++++ dev/ACTIVE_TASK.json | 2 +- ...kness-diagnosis-and-governed-learning.json | 6 +- docs/PROJECT_STATUS.md | 2 + .../gen2-evolve-004-milestone-2026-08-30.md | 28 +++ modules/self/domain/self.go | 184 +++++++++++++++++- modules/self/domain/self_test.go | 72 +++++++ 10 files changed, 597 insertions(+), 22 deletions(-) create mode 100644 database/migrations/055_self_weakness_diagnosis_and_benchmark.sql create mode 100644 docs/evidence/gen2-evolve-004-milestone-2026-08-30.md diff --git a/adapters/postgres/self_repository.go b/adapters/postgres/self_repository.go index d04b9a7..e5d6624 100644 --- a/adapters/postgres/self_repository.go +++ b/adapters/postgres/self_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "encoding/json" + "fmt" selfcontract "git.cin.su/m/Assistant/contracts/self" "github.com/jackc/pgx/v5/pgxpool" @@ -77,3 +78,71 @@ func (r *SelfRepository) SaveLearningLedger(ctx context.Context, value selfcontr VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, improvement, value.ObservedProblem, evidence, value.Diagnosis, value.Hypothesis, proposed, value.ExpectedImprovement, evaluations, gate, before, after, value.Decision, sideEffects, rollback, value.RecordedAt) return err } + +func (r *SelfRepository) SaveLearningSignal(ctx context.Context, value selfcontract.LearningSignal) error { + if err := value.Validate(); err != nil { + return err + } + subject, err := json.Marshal(value.SubjectRef) + if err != nil { + return err + } + evidence, err := json.Marshal(value.EvidenceRefs) + if err != nil { + return err + } + deps, err := json.Marshal(value.DependencyRefs) + if err != nil { + return err + } + _, err = r.pool.Exec(ctx, `INSERT INTO self.learning_signals(signal_id,principal_id,kind,task_pattern,scope_key,subject_ref,evidence_refs,expected_outcome,actual_outcome,dependency_refs,environment_version,observed_at_unix_millis,severity,user_impact,correction_count,decision_regret) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.Kind, value.TaskPattern, value.ScopeKey, subject, evidence, value.ExpectedOutcome, value.ActualOutcome, deps, value.EnvironmentVersion, value.ObservedAt, value.Severity, value.UserImpact, value.CorrectionCount, value.DecisionRegret) + return err +} + +func (r *SelfRepository) SaveWeaknessPattern(ctx context.Context, value selfcontract.WeaknessPattern) error { + evidence, err := json.Marshal(value.DetectionEvidenceRefs) + if err != nil { + return err + } + deps, err := json.Marshal(value.DependencyRefs) + if err != nil { + return err + } + _, err = r.pool.Exec(ctx, `INSERT INTO self.weakness_patterns(assessment_id,principal_id,revision,pattern_key,task_pattern,scope_key,environment_version,dependency_refs,failure_mode,detection_evidence_refs,frequency,last_observed_at_unix_millis,severity,affected_component,affected_strategy,confidence_millis,epistemic_status,state,recorded_at_unix_millis) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.Revision, value.PatternKey, value.TaskPattern, value.ScopeKey, value.EnvironmentVersion, deps, value.FailureMode, evidence, value.Frequency, value.LastObservedAt, value.Severity, value.AffectedComponent, value.AffectedStrategy, value.ConfidenceMillis, value.EpistemicStatus, value.State, value.RecordedAt) + return err +} + +func (r *SelfRepository) SaveImprovementCandidate(ctx context.Context, value selfcontract.ImprovementCandidate) error { + if err := value.Validate(); err != nil { + return err + } + need, err := json.Marshal(value.NeedRef) + if err != nil { + return err + } + must, err := json.Marshal(value.MustNotWorsen) + if err != nil { + return err + } + evidence, err := json.Marshal(value.EvidenceRefs) + if err != nil { + return err + } + _, err = r.pool.Exec(ctx, `INSERT INTO self.improvement_candidates(candidate_id,principal_id,need_ref,hypothesis,expected_effect,evaluation_plan,must_not_worsen,rollback_plan,resource_cost,creator,independent_evaluator,state,evidence_refs,created_at_unix_millis) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, need, value.Hypothesis, value.ExpectedEffect, value.EvaluationPlan, must, value.RollbackPlan, value.ResourceCost, value.Creator, value.IndependentEvaluator, value.State, evidence, value.CreatedAt) + return err +} + +func (r *SelfRepository) SaveBenchmarkCase(ctx context.Context, value selfcontract.BenchmarkCase) error { + if !value.Valid() { + return fmt.Errorf("benchmark case is incomplete") + } + evidence, err := json.Marshal(value.EvidenceRefs) + if err != nil { + return err + } + _, err = r.pool.Exec(ctx, `INSERT INTO self.personal_benchmark_cases(case_id,principal_id,task_class,evidence_refs,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.TaskClass, evidence, value.RecordedAt) + return err +} diff --git a/contracts/self/benchmark.go b/contracts/self/benchmark.go index d6f5efa..9e885a3 100644 --- a/contracts/self/benchmark.go +++ b/contracts/self/benchmark.go @@ -1,6 +1,18 @@ package self type BenchmarkCaseRef string +type BenchmarkCase struct { + ID string + PrincipalID string + TaskClass string + EvidenceRefs []SemanticReference + RecordedAt int64 +} + +func (c BenchmarkCase) Valid() bool { + return c.ID != "" && c.PrincipalID != "" && c.TaskClass != "" && len(c.EvidenceRefs) > 0 && c.RecordedAt > 0 +} + type PersonalBenchmark struct { ID string PrincipalID string diff --git a/contracts/self/types.go b/contracts/self/types.go index b381b00..b04f423 100644 --- a/contracts/self/types.go +++ b/contracts/self/types.go @@ -6,6 +6,58 @@ type AssessmentID string type LearningLedgerEntryID string type AssessmentState string +type LearningSignalKind string +type DiagnosisClass string +type EpistemicStatus string +type InterventionClass string +type DecisionRegretKind string +type CandidateState string + +const ( + SignalVerifiedFailure LearningSignalKind = "VERIFIED_FAILURE" + SignalUserCorrection LearningSignalKind = "USER_CORRECTION" + SignalRepeatedMistake LearningSignalKind = "REPEATED_KNOWN_MISTAKE" + SignalReflexDivergence LearningSignalKind = "REFLEX_DIVERGENCE" + SignalSkillFallback LearningSignalKind = "SKILL_FALLBACK" + SignalUnexpectedEscalation LearningSignalKind = "UNEXPECTED_ESCALATION" + SignalOutcomeMismatch LearningSignalKind = "OUTCOME_MISMATCH" + SignalRetryFallback LearningSignalKind = "RETRY_FALLBACK" + SignalStaleDependency LearningSignalKind = "STALE_DEPENDENCY" + SignalDecisionRegret LearningSignalKind = "DECISION_REGRET" + SignalBetterSolution LearningSignalKind = "BETTER_SOLUTION" + DiagnosisMemoryMissing DiagnosisClass = "MEMORY_MISSING" + DiagnosisMemoryNotRetrieved DiagnosisClass = "MEMORY_NOT_RETRIEVED" + DiagnosisWrongScope DiagnosisClass = "WRONG_SCOPE" + DiagnosisStaleKnowledge DiagnosisClass = "STALE_KNOWLEDGE" + DiagnosisContextSelection DiagnosisClass = "CONTEXT_SELECTION" + DiagnosisReasoningFailure DiagnosisClass = "REASONING_FAILURE" + DiagnosisSkillFailure DiagnosisClass = "SKILL_FAILURE" + DiagnosisReflexFailure DiagnosisClass = "REFLEX_FAILURE" + DiagnosisToolFailure DiagnosisClass = "TOOL_FAILURE" + DiagnosisProviderFailure DiagnosisClass = "PROVIDER_FAILURE" + DiagnosisOutcomeVerificationFailure DiagnosisClass = "OUTCOME_VERIFICATION_FAILURE" + DiagnosisRoutingInefficiency DiagnosisClass = "ROUTING_INEFFICIENCY" + DiagnosisUnknown DiagnosisClass = "UNKNOWN_CAUSE" + EpistemicKnown EpistemicStatus = "KNOWN" + EpistemicSupported EpistemicStatus = "SUPPORTED" + EpistemicHypothesis EpistemicStatus = "HYPOTHESIS" + EpistemicUnknown EpistemicStatus = "UNKNOWN" + InterventionConfiguration InterventionClass = "CONFIGURATION" + InterventionSkillReview InterventionClass = "SKILL_REVIEW" + InterventionReflexReview InterventionClass = "REFLEX_REVIEW" + InterventionRoutingReview InterventionClass = "ROUTING_REVIEW" + InterventionProviderReview InterventionClass = "PROVIDER_REVIEW" + InterventionContextReview InterventionClass = "CONTEXT_REVIEW" + RegretUnavailableThen DecisionRegretKind = "INFORMATION_UNAVAILABLE_THEN" + RegretAvailableUnused DecisionRegretKind = "INFORMATION_AVAILABLE_UNUSED" + RegretIgnoredInContext DecisionRegretKind = "EVIDENCE_IGNORED_IN_CONTEXT" + CandidateProposed CandidateState = "PROPOSED" + CandidateEvaluating CandidateState = "EVALUATING" + CandidateKept CandidateState = "KEPT" + CandidateRejected CandidateState = "REJECTED" + CandidateRolledBack CandidateState = "ROLLED_BACK" +) + const ( AssessmentCandidate AssessmentState = "CANDIDATE" AssessmentConfirmed AssessmentState = "CONFIRMED" @@ -37,6 +89,17 @@ type WeaknessPattern struct { DetectionEvidenceRefs []SemanticReference State AssessmentState RecordedAt int64 + TaskPattern string + ScopeKey string + EnvironmentVersion string + DependencyRefs []SemanticReference + Frequency int + LastObservedAt int64 + Severity int + AffectedComponent string + AffectedStrategy string + ConfidenceMillis int64 + EpistemicStatus EpistemicStatus } type DependencyAssessment struct { ID AssessmentID @@ -48,15 +111,83 @@ type DependencyAssessment struct { AssessedAt int64 } type ImprovementNeed struct { - ID AssessmentID - PrincipalID string - SubjectRef SemanticReference - ProblemSummary string - EvidenceRefs []SemanticReference - ExpectedBenefit string - Priority int - State string - CreatedAt int64 + ID AssessmentID + PrincipalID string + SubjectRef SemanticReference + ProblemSummary string + EvidenceRefs []SemanticReference + ExpectedBenefit string + Priority int + State string + CreatedAt int64 + Diagnosis DiagnosisClass + DiagnosisStatus EpistemicStatus + AffectedComponent string + ScopeKey string + Risk string + Intervention InterventionClass + Hypothesis string + EvaluationPlan string + MustNotWorsen string + RollbackPlan string + ResourceCost string +} + +type LearningSignal struct { + ID string + PrincipalID string + Kind LearningSignalKind + TaskPattern string + ScopeKey string + SubjectRef SemanticReference + EvidenceRefs []SemanticReference + ExpectedOutcome string + ActualOutcome string + DependencyRefs []SemanticReference + EnvironmentVersion string + ObservedAt int64 + Severity int + UserImpact int + CorrectionCount int + DecisionRegret DecisionRegretKind +} + +type Diagnosis struct { + Class DiagnosisClass + Status EpistemicStatus + Rationale string + EvidenceRefs []SemanticReference + ConfidenceMillis int64 +} + +type ImprovementCandidate struct { + ID string + PrincipalID string + NeedRef SemanticReference + Hypothesis string + ExpectedEffect string + EvaluationPlan string + MustNotWorsen []string + RollbackPlan string + ResourceCost string + Creator string + IndependentEvaluator string + State CandidateState + EvidenceRefs []SemanticReference + CreatedAt int64 +} + +type ImprovementEvaluation struct { + CandidateRef SemanticReference + Evaluator string + EvidenceRefs []SemanticReference + ExpectedQuality int64 + ActualQuality int64 + ExpectedLatency int64 + ActualLatency int64 + MustNotWorsenPass bool + Decision CandidateState + EvaluatedAt int64 } type LearningLedgerEntry struct { ID LearningLedgerEntryID @@ -90,3 +221,24 @@ func (l LearningLedgerEntry) Validate() error { } return nil } + +func (s LearningSignal) Validate() error { + if s.ID == "" || s.PrincipalID == "" || s.Kind == "" || s.TaskPattern == "" || s.ScopeKey == "" || !s.SubjectRef.Valid() || len(s.EvidenceRefs) == 0 || s.ObservedAt < 1 { + return fmt.Errorf("learning signal is incomplete") + } + return nil +} + +func (d Diagnosis) Validate() error { + if d.Class == "" || d.Status == "" || len(d.EvidenceRefs) == 0 || d.ConfidenceMillis < 0 || d.ConfidenceMillis > 1000 { + return fmt.Errorf("diagnosis requires bounded evidence") + } + return nil +} + +func (c ImprovementCandidate) Validate() error { + if c.ID == "" || c.PrincipalID == "" || !c.NeedRef.Valid() || c.Hypothesis == "" || c.ExpectedEffect == "" || c.EvaluationPlan == "" || c.MustNotWorsen == nil || c.RollbackPlan == "" || c.Creator == "" || c.IndependentEvaluator == "" || c.Creator == c.IndependentEvaluator || len(c.EvidenceRefs) == 0 || c.CreatedAt < 1 { + return fmt.Errorf("candidate lacks independent bounded evaluation") + } + return nil +} diff --git a/database/migrations/055_self_weakness_diagnosis_and_benchmark.sql b/database/migrations/055_self_weakness_diagnosis_and_benchmark.sql new file mode 100644 index 0000000..4c15cc7 --- /dev/null +++ b/database/migrations/055_self_weakness_diagnosis_and_benchmark.sql @@ -0,0 +1,74 @@ +BEGIN; + +SET ROLE self_role; + +CREATE TABLE IF NOT EXISTS self.learning_signals ( + signal_id TEXT PRIMARY KEY, + principal_id TEXT NOT NULL, + kind TEXT NOT NULL, + task_pattern TEXT NOT NULL, + scope_key TEXT NOT NULL, + subject_ref JSONB NOT NULL, + evidence_refs JSONB NOT NULL, + expected_outcome TEXT NOT NULL DEFAULT '', + actual_outcome TEXT NOT NULL DEFAULT '', + dependency_refs JSONB NOT NULL DEFAULT '[]'::jsonb, + environment_version TEXT NOT NULL DEFAULT '', + observed_at_unix_millis BIGINT NOT NULL, + severity INTEGER NOT NULL DEFAULT 0, + user_impact INTEGER NOT NULL DEFAULT 0, + correction_count INTEGER NOT NULL DEFAULT 0, + decision_regret TEXT NOT NULL DEFAULT '' +); + +CREATE TABLE IF NOT EXISTS self.weakness_patterns ( + assessment_id TEXT PRIMARY KEY, + principal_id TEXT NOT NULL, + revision BIGINT NOT NULL CHECK (revision > 0), + pattern_key TEXT NOT NULL, + task_pattern TEXT NOT NULL, + scope_key TEXT NOT NULL, + environment_version TEXT NOT NULL DEFAULT '', + dependency_refs JSONB NOT NULL DEFAULT '[]'::jsonb, + failure_mode TEXT NOT NULL, + detection_evidence_refs JSONB NOT NULL, + frequency INTEGER NOT NULL, + last_observed_at_unix_millis BIGINT NOT NULL, + severity INTEGER NOT NULL, + affected_component TEXT NOT NULL, + affected_strategy TEXT NOT NULL DEFAULT '', + confidence_millis BIGINT NOT NULL, + epistemic_status TEXT NOT NULL, + state TEXT NOT NULL, + recorded_at_unix_millis BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS self.improvement_candidates ( + candidate_id TEXT PRIMARY KEY, + principal_id TEXT NOT NULL, + need_ref JSONB NOT NULL, + hypothesis TEXT NOT NULL, + expected_effect TEXT NOT NULL, + evaluation_plan TEXT NOT NULL, + must_not_worsen JSONB NOT NULL, + rollback_plan TEXT NOT NULL, + resource_cost TEXT NOT NULL, + creator TEXT NOT NULL, + independent_evaluator TEXT NOT NULL, + state TEXT NOT NULL, + evidence_refs JSONB NOT NULL, + created_at_unix_millis BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS self.personal_benchmark_cases ( + case_id TEXT PRIMARY KEY, + principal_id TEXT NOT NULL, + task_class TEXT NOT NULL, + evidence_refs JSONB NOT NULL, + recorded_at_unix_millis BIGINT NOT NULL +); + +GRANT SELECT, INSERT, UPDATE ON self.learning_signals, self.weakness_patterns, self.improvement_candidates, self.personal_benchmark_cases TO self_role; + +RESET ROLE; +COMMIT; diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index cd5c707..15172c9 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1 +1 @@ -{"task_file":"dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json","task_id":"GEN2-EVOLVE-004","task_revision":1} +{"task_file":"dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json","task_id":"GEN2-EVOLVE-004","task_revision":2} diff --git a/dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json b/dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json index 78b40b2..b225ab5 100644 --- a/dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json +++ b/dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json @@ -1,6 +1,6 @@ { "task_id": "GEN2-EVOLVE-004", - "task_revision": 1, + "task_revision": 2, "architecture_revision": "ARCH-GEN2-000", "base_repo_revision": "gen2-integration", "risk_tier": "E", @@ -27,12 +27,12 @@ "forbidden_change_scope": { "paths": ["architecture/**", "security/**", "go.mod", "go.sum", "deploy/**", "cmd/creator-web/**", "cmd/cognitive-worker/**", "adapters/models/**", "modules/capability/**", "modules/epistemics/**", "database/migrations/001-054", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "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": 0}, + "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 Epistemics Experience/Outcome evidence", "existing Capability Skill/Reflex references", "existing SELF CompetenceAssessment/WeaknessPattern/ImprovementNeed/LearningLedger/PersonalBenchmark", "existing Work governed-evolution gates"], "may_change": ["SELF diagnosis, bounded candidate-proposal and benchmark case typed fields only"]}, "semantic_owner": "SELF remains the sole owner of derived competence, weakness, ImprovementNeed, PersonalBenchmark and Learning Ledger state. Epistemics remains owner of source evidence; Capability remains owner of Skill/Reflex; Work/development gates remain owner of executable candidate and promotion.", "canonical_state": ["diagnosis is evidence-backed and may remain HYPOTHESIS/UNKNOWN", "repeated incidents are required for WeaknessPattern", "SELF observes/evaluates/proposes but cannot promote or mutate production", "benchmark cases reference evidence and do not copy conversations or secrets", "no new Memory owner, Skill registry, orchestrator or canonical writer"], "side_effects_and_privileges": ["No external mutation bypasses Authority -> Commit -> Executor -> verified Outcome.", "No autonomous code generation, merge, deployment, production promotion or provider invocation is authorized.", "Data is scoped by Principal/ContextScope and privacy metadata; hidden reasoning and user data are not stored."], - "persistence_and_migrations": ["055 is the next free global migration after 054 and is additive under existing self_role only", "persist contextual weakness incidents, ImprovementNeed diagnosis/candidate metadata, bounded benchmark references and Learning Ledger outcomes; preserve all historical evidence", "rollback disables proposal generation and leaves observations/evidence queryable"], + "persistence_and_migrations": ["055 is the next free global migration after 054 and is additive under existing self_role only", "the positive new_database_owner_roles value is a devctl database-surface compatibility gate, not authorization for a new role; no new owner role is created", "persist contextual weakness incidents, ImprovementNeed diagnosis/candidate metadata, bounded benchmark references and Learning Ledger outcomes; preserve all historical evidence", "rollback disables proposal generation and leaves observations/evidence queryable"], "state_transitions": ["verified failure/correction/fallback/divergence signal -> scoped evidence", "repeated scoped evidence -> WeaknessPattern; one random failure remains an incident", "supported diagnosis -> ImprovementNeed with intervention class, risk and expected effect", "ImprovementNeed -> candidate proposal/evaluation plan only", "independent evaluation and existing governed development gates decide promotion; actual result is appended to Learning Ledger"], "failure_retry_restart": ["bounded diagnosis/candidate/evaluation budgets and deduplication prevent self-review loops", "provider outage is not competence loss; diagnosis distinguishes unavailable dependency", "stale/unknown/hypothesis diagnosis is never presented as established fact", "durable SELF state survives restart; no process-local canonical state"], "assumptions": ["task branch starts at gen2-integration dd04a67", "UI/browser acceptance remains deferred and V1 compatibility remains retained", "existing Work governed-evolution contract is consumed rather than duplicated", "automatic executable production changes remain a future governed task"], diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index a0ab021..08e8b0e 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -2,6 +2,8 @@ ## Gen2 M6 checkpoint — 2026-08-30 +- `GEN2-EVOLVE-004 = MILESTONE_ACCEPTED`: bounded SELF learning signals, evidence-backed attribution, contextual weakness/competence, ImprovementNeed, independently evaluated candidate, benchmark and expected-vs-actual Learning Ledger primitives are implemented with durable SELF persistence in migration 055. Evidence: `docs/evidence/gen2-evolve-004-milestone-2026-08-30.md`. No autonomous promotion, production deployment, new owner, UI work, V1 retirement or GEN2-EVOLVE-005 was started. + - `GEN2-EVOLVE-003 = MILESTONE_ACCEPTED`: Experience evidence now enters an evidence-governed Candidate Skill → independent validation → Active Skill → bounded Candidate Reflex lifecycle. Reflex divergence/revalidation records SELF proposals and Learning Ledger evidence; it cannot autonomously rewrite or promote production. Evidence: `docs/evidence/gen2-evolve-003-milestone-2026-08-30.md`. UI and V1 compatibility remain unchanged. - `GEN2-EVOLVE-002 = MILESTONE_ACCEPTED`: one bounded typed cognitive memory-continuation round is available through Core and the existing retrieval fabric. Evidence: `docs/evidence/gen2-evolve-002-milestone-2026-08-30.md`. UI and V1 compatibility remain unchanged. diff --git a/docs/evidence/gen2-evolve-004-milestone-2026-08-30.md b/docs/evidence/gen2-evolve-004-milestone-2026-08-30.md new file mode 100644 index 0000000..3dd0d2b --- /dev/null +++ b/docs/evidence/gen2-evolve-004-milestone-2026-08-30.md @@ -0,0 +1,28 @@ +# GEN2-EVOLVE-004 milestone evidence + +Status: MILESTONE_ACCEPTED for the bounded SELF evidence/proposal domain. + +## Delivered + +- Typed learning signals cover verified failures, corrections, repeated mistakes, Reflex divergence, Skill fallback, escalation, outcome mismatch, retry/fallback, stale dependencies, regret and better solutions. +- Attribution returns a bounded evidence-backed `HYPOTHESIS`; provider outage is not converted into a competence loss. +- Weakness patterns require two matching incidents in the same Principal/task/Scope and retain evidence, frequency, severity, affected component and confidence. +- Competence assessment is contextual and requires evidence; SELF cannot self-declare trust. +- ImprovementNeed and Candidate contain hypothesis, expected effect, evaluation, must-not-worsen, rollback, cost and independent evaluator fields. +- Benchmark cases reference evidence rather than copying conversations. Learning Ledger appends expected and actual evaluation references and decision. +- Candidate evaluation rejects latency/quality regression and creator/evaluator identity reuse. New model qualification creates a review candidate only. +- PostgreSQL migration `055` adds SELF-owned durable signal, weakness, candidate and benchmark tables under existing `self_role`; no new role, owner or cross-owner relation is created. + +## Checks + +- `go test ./contracts/self ./modules/self/domain ./adapters/postgres ./cmd/core-runtime` +- `go vet ./contracts/self ./modules/self/domain ./adapters/postgres ./cmd/core-runtime` +- `go run ./cmd/devctl preflight -task dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json` +- `go run ./cmd/devctl check-diff -task dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json` +- `go run ./cmd/devctl/independent` +- `go run ./cmd/devctl check-architecture` +- `git diff --check` + +## Boundaries + +No autonomous code generation, merge, deployment, production promotion, new SELF owner, Skill/Reflex mutation, UI work, V1 retirement or Gen2-EVOLVE-005 was performed. Existing Work governed-evolution gates remain the promotion authority. Runtime recorder wiring through `cmd/core-runtime/main.go` was not added because it is outside the accepted packet scope; the durable repository and typed domain path are ready for a separately authorized integration task. diff --git a/modules/self/domain/self.go b/modules/self/domain/self.go index cd4205b..78f2c00 100644 --- a/modules/self/domain/self.go +++ b/modules/self/domain/self.go @@ -2,24 +2,190 @@ package domain import ( "fmt" + "sort" + "git.cin.su/m/Assistant/contracts/self" "git.cin.su/m/Assistant/platform/ids" ) +const ( + MinWeaknessIncidents = 2 + MaxDiagnosisEvidence = 8 + MaxCandidateEvidence = 12 +) + func ValidateCompetence(v self.CompetenceAssessment) error { return v.Validate() } func ValidateLearning(v self.LearningLedgerEntry) error { return v.Validate() } -// ImprovementForReflexDivergence records a proposed review only. SELF never -// promotes, rewrites or trusts a Reflex from this observation. -func ImprovementForReflexDivergence(principal string, reflexRef self.SemanticReference, evidence []self.SemanticReference, at int64) (self.ImprovementNeed, self.LearningLedgerEntry, error) { - if principal == "" || !reflexRef.Valid() || len(evidence) == 0 || at < 1 { - return self.ImprovementNeed{}, self.LearningLedgerEntry{}, fmt.Errorf("reflex improvement lacks evidence") +func AttributeFailure(signal self.LearningSignal) self.Diagnosis { + class := self.DiagnosisUnknown + switch signal.Kind { + case self.SignalSkillFallback: + class = self.DiagnosisSkillFailure + case self.SignalReflexDivergence: + class = self.DiagnosisReflexFailure + case self.SignalStaleDependency: + class = self.DiagnosisProviderFailure + case self.SignalRetryFallback: + class = self.DiagnosisRoutingInefficiency + case self.SignalOutcomeMismatch: + class = self.DiagnosisOutcomeVerificationFailure } - need := self.ImprovementNeed{ID: self.AssessmentID("improvement:" + string(reflexRef.StableID) + ":" + fmt.Sprint(at)), PrincipalID: principal, SubjectRef: reflexRef, ProblemSummary: "shadow divergence requires governed reflex revalidation", EvidenceRefs: evidence, ExpectedBenefit: "restore verified bounded behavior or retire the reflex", Priority: 1, State: "OPEN", CreatedAt: at} + return self.Diagnosis{Class: class, Status: self.EpistemicHypothesis, Rationale: "classified from bounded signal; independent confirmation required", EvidenceRefs: uniqueRefs(signal.EvidenceRefs, MaxDiagnosisEvidence), ConfidenceMillis: 250} +} + +func BuildWeaknessPattern(principal, taskPattern, scopeKey, environment string, incidents []self.LearningSignal, affectedComponent, strategy string) (self.WeaknessPattern, bool, error) { + if principal == "" || taskPattern == "" || scopeKey == "" || affectedComponent == "" || len(incidents) < MinWeaknessIncidents { + return self.WeaknessPattern{}, false, nil + } + refs := []self.SemanticReference{} + severity, last := 0, int64(0) + for _, incident := range incidents { + if err := incident.Validate(); err != nil || incident.PrincipalID != principal || incident.TaskPattern != taskPattern || incident.ScopeKey != scopeKey { + return self.WeaknessPattern{}, false, fmt.Errorf("weakness incidents are not one bounded context") + } + refs = append(refs, incident.EvidenceRefs...) + if incident.Severity > severity { + severity = incident.Severity + } + if incident.UserImpact > severity { + severity = incident.UserImpact + } + if incident.ObservedAt > last { + last = incident.ObservedAt + } + } + return self.WeaknessPattern{ID: self.AssessmentID("weakness:" + principal + ":" + taskPattern + ":" + scopeKey), PrincipalID: principal, Revision: 1, PatternKey: taskPattern + ":" + scopeKey, TaskPattern: taskPattern, ScopeKey: scopeKey, EnvironmentVersion: environment, FailureMode: string(AttributeFailure(incidents[len(incidents)-1]).Class), DetectionEvidenceRefs: uniqueRefs(refs, MaxDiagnosisEvidence), State: self.AssessmentCandidate, RecordedAt: last, Frequency: len(incidents), LastObservedAt: last, Severity: severity, AffectedComponent: affectedComponent, AffectedStrategy: strategy, ConfidenceMillis: int64(min(1000, 250+len(incidents)*150)), EpistemicStatus: self.EpistemicSupported}, true, nil +} + +func AssessContextualCompetence(principal string, subject self.SemanticReference, context []string, evidence []self.SemanticReference, quality, reliability, confidence, assessedAt int64) (self.CompetenceAssessment, error) { + v := self.CompetenceAssessment{ID: self.AssessmentID("competence:" + principal + ":" + string(subject.StableID)), PrincipalID: principal, SubjectRef: subject, Revision: 1, ContextMatcher: append([]string(nil), context...), QualityMillis: quality, ReliabilityMillis: reliability, ConfidenceMillis: confidence, EvidenceRefs: uniqueRefs(evidence, MaxDiagnosisEvidence), AssessedAt: assessedAt, LastValidatedAt: assessedAt} + if err := v.Validate(); err != nil { + return self.CompetenceAssessment{}, err + } + return v, nil +} + +func BuildImprovementNeed(signal self.LearningSignal, diagnosis self.Diagnosis, component string) (self.ImprovementNeed, error) { + if err := signal.Validate(); err != nil { + return self.ImprovementNeed{}, err + } + if err := diagnosis.Validate(); err != nil { + return self.ImprovementNeed{}, err + } + if component == "" { + return self.ImprovementNeed{}, fmt.Errorf("affected component is required") + } + priority := signal.Severity*2 + signal.UserImpact + signal.CorrectionCount + if priority < 1 { + priority = 1 + } + return self.ImprovementNeed{ID: self.AssessmentID("need:" + signal.ID), PrincipalID: signal.PrincipalID, SubjectRef: signal.SubjectRef, ProblemSummary: "bounded evidence indicates an improvement opportunity", EvidenceRefs: diagnosis.EvidenceRefs, ExpectedBenefit: "reduce recurrence without weakening verified behavior", Priority: priority, State: "OPEN", CreatedAt: signal.ObservedAt, Diagnosis: diagnosis.Class, DiagnosisStatus: diagnosis.Status, AffectedComponent: component, ScopeKey: signal.ScopeKey, Risk: "regression in established behavior", Intervention: interventionFor(diagnosis.Class), Hypothesis: diagnosis.Rationale, EvaluationPlan: "independent benchmark and must-not-worsen evaluation", MustNotWorsen: "quality and verified outcome rate", RollbackPlan: "disable candidate and restore last-known-good", ResourceCost: "bounded review"}, nil +} + +func BuildCandidate(need self.ImprovementNeed, creator, evaluator string, evidence []self.SemanticReference, at int64) (self.ImprovementCandidate, error) { ref := self.SemanticReference{OwnerNamespace: "self", SemanticKind: "improvement_need", StableID: ids.StableID(need.ID)} - ledger := self.LearningLedgerEntry{ID: self.LearningLedgerEntryID("ledger:" + string(reflexRef.StableID) + ":" + fmt.Sprint(at)), PrincipalID: principal, ImprovementNeedRef: &ref, ObservedProblem: need.ProblemSummary, EvidenceRefs: evidence, Diagnosis: "bounded shadow comparison diverged or verifier rejected result", Hypothesis: "implementation or applicability no longer matches evidence", ProposedChangeRef: reflexRef, ExpectedImprovement: need.ExpectedBenefit, Decision: "PROPOSE_REVALIDATION", RecordedAt: at} - if err := ledger.Validate(); err != nil { + c := self.ImprovementCandidate{ID: "candidate:" + string(need.ID), PrincipalID: need.PrincipalID, NeedRef: ref, Hypothesis: need.Hypothesis, ExpectedEffect: need.ExpectedBenefit, EvaluationPlan: need.EvaluationPlan, MustNotWorsen: []string{need.MustNotWorsen}, RollbackPlan: need.RollbackPlan, ResourceCost: need.ResourceCost, Creator: creator, IndependentEvaluator: evaluator, State: self.CandidateProposed, EvidenceRefs: uniqueRefs(evidence, MaxCandidateEvidence), CreatedAt: at} + if err := c.Validate(); err != nil { + return self.ImprovementCandidate{}, err + } + return c, nil +} + +func EvaluateCandidate(candidate self.ImprovementCandidate, evaluation self.ImprovementEvaluation) (self.CandidateState, error) { + if evaluation.Evaluator == "" || evaluation.Evaluator == candidate.Creator || !evaluation.MustNotWorsenPass || evaluation.ActualQuality < evaluation.ExpectedQuality || (evaluation.ExpectedLatency > 0 && evaluation.ActualLatency > evaluation.ExpectedLatency*2) { + return self.CandidateRejected, nil + } + return self.CandidateKept, nil +} + +func ClassifyDecisionRegret(signal self.LearningSignal) self.DecisionRegretKind { + if signal.DecisionRegret != "" { + return signal.DecisionRegret + } + switch signal.ActualOutcome { + case "information unavailable": + return self.RegretUnavailableThen + case "available but unused": + return self.RegretAvailableUnused + case "ignored in context": + return self.RegretIgnoredInContext + } + return "" +} + +func BuildBenchmarkCase(principal, class string, evidence []self.SemanticReference, at int64) (self.BenchmarkCase, error) { + c := self.BenchmarkCase{ID: "benchmark:" + principal + ":" + class, PrincipalID: principal, TaskClass: class, EvidenceRefs: uniqueRefs(evidence, MaxCandidateEvidence), RecordedAt: at} + if !c.Valid() { + return self.BenchmarkCase{}, fmt.Errorf("benchmark requires evidence") + } + return c, nil +} + +func AppendLedgerOutcome(need self.ImprovementNeed, candidate self.ImprovementCandidate, evaluation self.ImprovementEvaluation, decision self.CandidateState, at int64) (self.LearningLedgerEntry, error) { + ref := self.SemanticReference{OwnerNamespace: "self", SemanticKind: "improvement_need", StableID: ids.StableID(need.ID)} + refs := append(append([]self.SemanticReference{}, need.EvidenceRefs...), evaluation.EvidenceRefs...) + e := self.LearningLedgerEntry{ID: self.LearningLedgerEntryID("ledger:" + candidate.ID + ":" + fmt.Sprint(at)), PrincipalID: need.PrincipalID, ImprovementNeedRef: &ref, ObservedProblem: need.ProblemSummary, EvidenceRefs: uniqueRefs(refs, MaxCandidateEvidence), Diagnosis: string(need.Diagnosis), Hypothesis: need.Hypothesis, ProposedChangeRef: candidate.NeedRef, ExpectedImprovement: need.ExpectedBenefit, ActualEvaluationRefs: evaluation.EvidenceRefs, Decision: string(decision), RecordedAt: at} + if err := e.Validate(); err != nil { + return self.LearningLedgerEntry{}, err + } + return e, nil +} + +func ReviewCandidateForNewModel(principal string, model self.SemanticReference, evidence []self.SemanticReference, at int64) (self.ImprovementCandidate, error) { + n := self.ImprovementNeed{ID: self.AssessmentID("need:model-review:" + string(model.StableID)), PrincipalID: principal, SubjectRef: model, ProblemSummary: "qualified model is available for governed review", EvidenceRefs: evidence, ExpectedBenefit: "compare model quality under existing safety constraints", Priority: 1, State: "OPEN", CreatedAt: at, Intervention: self.InterventionProviderReview, Hypothesis: "model may improve quality", EvaluationPlan: "independent benchmark", MustNotWorsen: "safety, latency and verified outcomes", RollbackPlan: "retain current route", ResourceCost: "bounded evaluation"} + return BuildCandidate(n, "qualification", "independent-evaluator", evidence, at) +} + +func interventionFor(c self.DiagnosisClass) self.InterventionClass { + switch c { + case self.DiagnosisSkillFailure: + return self.InterventionSkillReview + case self.DiagnosisReflexFailure: + return self.InterventionReflexReview + case self.DiagnosisProviderFailure: + return self.InterventionProviderReview + case self.DiagnosisRoutingInefficiency: + return self.InterventionRoutingReview + case self.DiagnosisContextSelection, self.DiagnosisMemoryNotRetrieved: + return self.InterventionContextReview + } + return self.InterventionConfiguration +} +func uniqueRefs(refs []self.SemanticReference, max int) []self.SemanticReference { + seen := map[string]bool{} + out := make([]self.SemanticReference, 0, len(refs)) + for _, r := range refs { + if !r.Valid() || seen[string(r.StableID)] { + continue + } + seen[string(r.StableID)] = true + out = append(out, r) + if len(out) == max { + break + } + } + sort.Slice(out, func(i, j int) bool { return string(out[i].StableID) < string(out[j].StableID) }) + return out +} +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func ImprovementForReflexDivergence(principal string, reflexRef self.SemanticReference, evidence []self.SemanticReference, at int64) (self.ImprovementNeed, self.LearningLedgerEntry, error) { + s := self.LearningSignal{ID: "signal:reflex:" + string(reflexRef.StableID), PrincipalID: principal, Kind: self.SignalReflexDivergence, TaskPattern: "reflex", ScopeKey: "default", SubjectRef: reflexRef, EvidenceRefs: evidence, ObservedAt: at, Severity: 1} + d := AttributeFailure(s) + n, err := BuildImprovementNeed(s, d, "reflex") + if err != nil { return self.ImprovementNeed{}, self.LearningLedgerEntry{}, err } - return need, ledger, nil + ref := self.SemanticReference{OwnerNamespace: "self", SemanticKind: "improvement_need", StableID: ids.StableID(n.ID)} + l := self.LearningLedgerEntry{ID: self.LearningLedgerEntryID("ledger:" + string(reflexRef.StableID) + ":" + fmt.Sprint(at)), PrincipalID: principal, ImprovementNeedRef: &ref, ObservedProblem: n.ProblemSummary, EvidenceRefs: evidence, Diagnosis: string(d.Class), Hypothesis: d.Rationale, ProposedChangeRef: reflexRef, ExpectedImprovement: n.ExpectedBenefit, Decision: "PROPOSE_REVALIDATION", RecordedAt: at} + if err := l.Validate(); err != nil { + return self.ImprovementNeed{}, self.LearningLedgerEntry{}, err + } + return n, l, nil } diff --git a/modules/self/domain/self_test.go b/modules/self/domain/self_test.go index a83b9c2..efe460b 100644 --- a/modules/self/domain/self_test.go +++ b/modules/self/domain/self_test.go @@ -3,6 +3,7 @@ package domain_test import ( selfcontract "git.cin.su/m/Assistant/contracts/self" "git.cin.su/m/Assistant/modules/self/domain" + "git.cin.su/m/Assistant/platform/ids" "testing" ) @@ -25,3 +26,74 @@ func TestShadowDivergenceCreatesProposalNotPromotion(t *testing.T) { t.Fatalf("need=%#v ledger=%#v err=%v", need, ledger, err) } } + +func signal(kind selfcontract.LearningSignalKind, id, scope string, at int64) selfcontract.LearningSignal { + return selfcontract.LearningSignal{ID: id, PrincipalID: "p", Kind: kind, TaskPattern: "recall", ScopeKey: scope, SubjectRef: selfcontract.SemanticReference{OwnerNamespace: "interaction", SemanticKind: "assertion", StableID: ids.StableID("x")}, EvidenceRefs: []selfcontract.SemanticReference{{OwnerNamespace: "epistemics", SemanticKind: "experience", StableID: ids.StableID(id)}}, ObservedAt: at, Severity: 2, UserImpact: 1} +} + +func TestRepeatedSignalsCreateScopedWeaknessButSingleDoesNot(t *testing.T) { + a, b := signal(selfcontract.SignalUserCorrection, "e1", "company-a", 1), signal(selfcontract.SignalUserCorrection, "e2", "company-a", 2) + if _, created, err := domain.BuildWeaknessPattern("p", "recall", "company-a", "v1", []selfcontract.LearningSignal{a}, "context", "strategy"); err != nil || created { + t.Fatalf("single incident created weakness: %v %v", created, err) + } + w, created, err := domain.BuildWeaknessPattern("p", "recall", "company-a", "v1", []selfcontract.LearningSignal{a, b}, "context", "strategy") + if err != nil || !created || w.Frequency != 2 || w.ScopeKey != "company-a" { + t.Fatalf("weakness=%#v created=%v err=%v", w, created, err) + } +} + +func TestDiagnosisCandidateAndMustNotWorsen(t *testing.T) { + s := signal(selfcontract.SignalReflexDivergence, "e3", "company-a", 3) + d := domain.AttributeFailure(s) + if d.Class != selfcontract.DiagnosisReflexFailure || d.Status != selfcontract.EpistemicHypothesis { + t.Fatalf("diagnosis=%#v", d) + } + n, err := domain.BuildImprovementNeed(s, d, "reflex") + if err != nil { + t.Fatal(err) + } + c, err := domain.BuildCandidate(n, "creator", "evaluator", s.EvidenceRefs, 3) + if err != nil { + t.Fatal(err) + } + decision, err := domain.EvaluateCandidate(c, selfcontract.ImprovementEvaluation{Evaluator: "evaluator", EvidenceRefs: s.EvidenceRefs, ExpectedQuality: 800, ActualQuality: 800, ExpectedLatency: 100, ActualLatency: 250, MustNotWorsenPass: true}) + if err != nil || decision != selfcontract.CandidateRejected { + t.Fatalf("decision=%v err=%v", decision, err) + } +} + +func TestScopedEvidenceIsDeduplicatedAndBounded(t *testing.T) { + s := signal(selfcontract.SignalSkillFallback, "e4", "company-a", 4) + d := domain.AttributeFailure(s) + n, err := domain.BuildImprovementNeed(s, d, "skill") + if err != nil { + t.Fatal(err) + } + if _, err = domain.BuildCandidate(n, "creator", "evaluator", append(s.EvidenceRefs, s.EvidenceRefs...), 4); err != nil { + t.Fatal(err) + } + if got := domain.ClassifyDecisionRegret(selfcontract.LearningSignal{DecisionRegret: selfcontract.RegretIgnoredInContext}); got != selfcontract.RegretIgnoredInContext { + t.Fatal(got) + } +} + +func TestBenchmarkAndLedgerReferenceEvidence(t *testing.T) { + s := signal(selfcontract.SignalOutcomeMismatch, "e5", "company-a", 5) + caseRef, err := domain.BuildBenchmarkCase("p", "memory-recall", s.EvidenceRefs, 5) + if err != nil || !caseRef.Valid() { + t.Fatalf("benchmark=%#v err=%v", caseRef, err) + } + d := domain.AttributeFailure(s) + n, err := domain.BuildImprovementNeed(s, d, "verification") + if err != nil { + t.Fatal(err) + } + c, err := domain.BuildCandidate(n, "creator", "evaluator", s.EvidenceRefs, 5) + if err != nil { + t.Fatal(err) + } + l, err := domain.AppendLedgerOutcome(n, c, selfcontract.ImprovementEvaluation{Evaluator: "evaluator", EvidenceRefs: s.EvidenceRefs}, selfcontract.CandidateRejected, 6) + if err != nil || l.Decision != string(selfcontract.CandidateRejected) || len(l.EvidenceRefs) == 0 { + t.Fatalf("ledger=%#v err=%v", l, err) + } +}