diff --git a/adapters/postgres/capability_repository.go b/adapters/postgres/capability_repository.go index bf5bca4..a271daa 100644 --- a/adapters/postgres/capability_repository.go +++ b/adapters/postgres/capability_repository.go @@ -2,6 +2,7 @@ package postgres import ( "context" + "encoding/json" "errors" "fmt" @@ -305,12 +306,14 @@ func (r *CapabilityRepository) SaveSkillProfile(ctx context.Context, value capab _, err = tx.Exec(ctx, `INSERT INTO capability.skill_profiles ( skill_profile_id,revision,name,description,applicable_context,implementation_ref,implementation_version,status, supporting_experience_refs,known_failure_experience_refs,confidence_millis,last_validated_unix_millis, - observed_failure_count,observed_correction_count,recorded_at_unix_millis) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT DO NOTHING`, + observed_failure_count,observed_correction_count,recorded_at_unix_millis,applicability,dependency_refs,validation_evidence_refs, + independent_evaluation_refs,known_failure_conditions,verified_outcome_count,must_not_worsen_satisfied) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) ON CONFLICT DO NOTHING`, value.ID, value.Revision, value.Name, value.Description, value.ApplicableContext, value.ImplementationRef, value.ImplementationVersion, value.Status, value.SupportingExperienceRefs, value.KnownFailureExperienceRefs, value.ConfidenceMillis, value.LastValidatedUnixMillis, value.ObservedFailureCount, value.ObservedCorrectionCount, - value.RecordedAtUnixMillis) + value.RecordedAtUnixMillis, mustJSON(value.Applicability), value.DependencyRefs, value.ValidationEvidenceRefs, + value.IndependentEvaluationRefs, value.KnownFailureConditions, value.VerifiedOutcomeCount, value.MustNotWorsenSatisfied) if err != nil { return err } @@ -329,13 +332,16 @@ func (r *CapabilityRepository) LoadSkillProfile(ctx context.Context, id capabili err := r.pool.QueryRow(ctx, `SELECT p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status, p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis, - p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis + p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis,p.applicability,p.dependency_refs, + p.validation_evidence_refs,p.independent_evaluation_refs,p.known_failure_conditions,p.verified_outcome_count,p.must_not_worsen_satisfied FROM capability.skill_profile_heads h JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision WHERE p.skill_profile_id=$1`, id).Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext, &value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs, &value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis, - &value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis) + &value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis, newJSONTarget(&value.Applicability), + &value.DependencyRefs, &value.ValidationEvidenceRefs, &value.IndependentEvaluationRefs, &value.KnownFailureConditions, + &value.VerifiedOutcomeCount, &value.MustNotWorsenSatisfied) if err != nil { return capability.SkillProfile{}, fmt.Errorf("load skill profile: %w", err) } @@ -349,7 +355,8 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in rows, err := r.pool.Query(ctx, `SELECT p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status, p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis, - p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis + p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis,p.applicability,p.dependency_refs, + p.validation_evidence_refs,p.independent_evaluation_refs,p.known_failure_conditions,p.verified_outcome_count,p.must_not_worsen_satisfied FROM capability.skill_profile_heads h JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision ORDER BY h.updated_at DESC, p.skill_profile_id LIMIT $1`, limit) @@ -363,7 +370,9 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in if err := rows.Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext, &value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs, &value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis, - &value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis); err != nil { + &value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis, newJSONTarget(&value.Applicability), + &value.DependencyRefs, &value.ValidationEvidenceRefs, &value.IndependentEvaluationRefs, &value.KnownFailureConditions, + &value.VerifiedOutcomeCount, &value.MustNotWorsenSatisfied); err != nil { return nil, err } result = append(result, value) @@ -371,6 +380,40 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in return result, rows.Err() } +func (r *CapabilityRepository) SaveReflexRevision(ctx context.Context, reflex capability.Reflex, value capability.ReflexRevision) error { + _, err := r.pool.Exec(ctx, `INSERT INTO capability.reflex_revisions ( + reflex_id,revision,principal_id,source_skill_id,source_skill_revision,kind,applicability,implementation_ref,implementation_version,artifact_digest, + dependency_refs,validation_evidence_refs,known_failure_experience_refs,shadow_policy,revalidation_policy,last_validated_at,lifecycle, + supersedes_revision,execution_count,failure_count,divergence_count,recorded_at) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) ON CONFLICT DO NOTHING`, + value.ReflexID, value.Revision, reflex.PrincipalID, reflex.SourceSkillRef, value.SourceSkillRevision, value.Kind, mustJSON(value.Applicability), value.ImplementationRef, value.ImplementationVersion, value.ArtifactDigest, + value.DependencyRefs, value.ValidationEvidenceRefs, value.KnownFailureExperienceRefs, mustJSON(value.Shadow), mustJSON(value.Revalidation), value.LastValidatedAt, value.Lifecycle, + value.SupersedesRevision, value.ExecutionCount, value.FailureCount, value.DivergenceCount, value.RecordedAt) + if err != nil { + return err + } + _, err = r.pool.Exec(ctx, `INSERT INTO capability.reflex_heads(reflex_id,revision,lifecycle) VALUES($1,$2,$3) + ON CONFLICT(reflex_id) DO UPDATE SET revision=EXCLUDED.revision,lifecycle=EXCLUDED.lifecycle,updated_at=CURRENT_TIMESTAMP + WHERE capability.reflex_heads.revision < EXCLUDED.revision`, value.ReflexID, value.Revision, value.Lifecycle) + return err +} + +func mustJSON(value any) []byte { encoded, _ := json.Marshal(value); return encoded } + +type jsonTarget struct{ target any } + +func newJSONTarget(target any) *jsonTarget { return &jsonTarget{target: target} } +func (v *jsonTarget) Scan(source any) error { + if source == nil { + return nil + } + bytes, ok := source.([]byte) + if !ok { + bytes = []byte(fmt.Sprint(source)) + } + return json.Unmarshal(bytes, v.target) +} + func (r *CapabilityRepository) LoadBinding(ctx context.Context, id capability.CapabilityBindingID, revision int64) (capability.Binding, error) { var binding capability.Binding err := r.pool.QueryRow(ctx, `SELECT binding_id,capability_id,owner_principal_id,revision,state,https_origin,delivery_path,receipt_path, diff --git a/adapters/postgres/self_repository.go b/adapters/postgres/self_repository.go new file mode 100644 index 0000000..d04b9a7 --- /dev/null +++ b/adapters/postgres/self_repository.go @@ -0,0 +1,79 @@ +package postgres + +import ( + "context" + "encoding/json" + + selfcontract "git.cin.su/m/Assistant/contracts/self" + "github.com/jackc/pgx/v5/pgxpool" +) + +// SelfRepository persists only SELF-owned derived assessments and ledger +// evidence. It never evaluates, promotes or changes Capability state. +type SelfRepository struct{ pool *pgxpool.Pool } + +func OpenSelf(ctx context.Context, config ContinuityConfig) (*SelfRepository, error) { + pool, err := openPool(ctx, config) + if err != nil { + return nil, err + } + return &SelfRepository{pool: pool}, nil +} + +func (r *SelfRepository) Close() { r.pool.Close() } + +func (r *SelfRepository) SaveImprovementNeed(ctx context.Context, value selfcontract.ImprovementNeed) error { + subject, err := json.Marshal(value.SubjectRef) + 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_needs(improvement_need_id,principal_id,subject_ref,problem_summary,evidence_refs,expected_benefit,priority,state,created_at_unix_millis) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, subject, value.ProblemSummary, evidence, value.ExpectedBenefit, value.Priority, value.State, value.CreatedAt) + return err +} + +func (r *SelfRepository) SaveLearningLedger(ctx context.Context, value selfcontract.LearningLedgerEntry) error { + evidence, err := json.Marshal(value.EvidenceRefs) + if err != nil { + return err + } + improvement, err := json.Marshal(value.ImprovementNeedRef) + if err != nil { + return err + } + proposed, err := json.Marshal(value.ProposedChangeRef) + if err != nil { + return err + } + evaluations, err := json.Marshal(value.ActualEvaluationRefs) + if err != nil { + return err + } + gate, err := json.Marshal(value.DevelopmentGateRef) + if err != nil { + return err + } + before, err := json.Marshal(value.BeforeVersionRefs) + if err != nil { + return err + } + after, err := json.Marshal(value.AfterVersionRefs) + if err != nil { + return err + } + sideEffects, err := json.Marshal(value.SideEffects) + if err != nil { + return err + } + rollback, err := json.Marshal(value.RollbackRef) + if err != nil { + return err + } + _, err = r.pool.Exec(ctx, `INSERT INTO self.learning_ledger(entry_id,principal_id,improvement_need_ref,observed_problem,evidence_refs,diagnosis,hypothesis,proposed_change_ref,expected_improvement,evaluation_refs,development_gate_ref,before_version_refs,after_version_refs,decision,side_effects,rollback_ref,recorded_at_unix_millis) + 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 +} diff --git a/contracts/capability/intelligence.go b/contracts/capability/intelligence.go index 8eab948..c1fd9b5 100644 --- a/contracts/capability/intelligence.go +++ b/contracts/capability/intelligence.go @@ -27,15 +27,30 @@ type StrategyDecision struct { Escalate bool } +// StrategyEligibility is supplied by Capability lifecycle resolution. It keeps +// a requested cheap route from becoming an entitlement to use stale evidence. +type StrategyEligibility struct { + RuleEligible bool + ReflexEligible bool + SkillEligible bool +} + func (r StrategyRequest) Select() StrategyDecision { + return r.SelectEligible(StrategyEligibility{RuleEligible: true, ReflexEligible: true, SkillEligible: true}) +} + +func (r StrategyRequest) SelectEligible(eligible StrategyEligibility) StrategyDecision { if r.RiskClass == "HIGH" || r.CostOfError >= 800 { return StrategyDecision{Path: PathStrongLocal, Reason: "high consequence requires deeper reasoning", Escalate: true} } - if r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 { + if eligible.RuleEligible && r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 { return StrategyDecision{Path: PathRule, Reason: "bounded low-risk path"} } - if r.PreferLowLatency { + if eligible.ReflexEligible && r.PreferLowLatency { return StrategyDecision{Path: PathReflex, Reason: "latency preference with later escalation"} } - return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"} + if eligible.SkillEligible { + return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"} + } + return StrategyDecision{Path: PathSmallLocal, Reason: "no eligible reflex or skill; escalate to reasoning", Escalate: true} } diff --git a/contracts/capability/reflex_v2.go b/contracts/capability/reflex_v2.go index 7c67249..8d1558e 100644 --- a/contracts/capability/reflex_v2.go +++ b/contracts/capability/reflex_v2.go @@ -10,6 +10,7 @@ const ( ReflexActive ReflexLifecycle = "ACTIVE" ReflexDegraded ReflexLifecycle = "DEGRADED" ReflexStale ReflexLifecycle = "STALE" + ReflexRevalidating ReflexLifecycle = "REVALIDATING" ReflexSuspended ReflexLifecycle = "SUSPENDED" ReflexRetired ReflexLifecycle = "RETIRED" ReflexDeterministicCode ReflexKind = "DETERMINISTIC_CODE" @@ -58,4 +59,19 @@ type ReflexRevision struct { LastValidatedAt int64 Lifecycle ReflexLifecycle RecordedAt int64 + SupersedesRevision int64 + ExecutionCount int64 + FailureCount int64 + DivergenceCount int64 +} + +// ShadowComparison is evidence produced by a bounded shadow sample. It is not +// permission to rewrite a Reflex; SELF/governed evaluation decide next steps. +type ShadowComparison struct { + ReflexID ReflexID + ReflexRevision int64 + EvidenceRefs []SkillEvidenceRef + VerifierPassed bool + Diverged bool + ObservedAt int64 } diff --git a/contracts/capability/resolution.go b/contracts/capability/resolution.go index ff7696f..070eab9 100644 --- a/contracts/capability/resolution.go +++ b/contracts/capability/resolution.go @@ -70,6 +70,7 @@ const ( SkillCandidate SkillLifecycleStatus = "CANDIDATE" SkillEvaluating SkillLifecycleStatus = "EVALUATING" SkillValidated SkillLifecycleStatus = "VALIDATED" + SkillActive SkillLifecycleStatus = "ACTIVE" SkillDegraded SkillLifecycleStatus = "DEGRADED" SkillSuspended SkillLifecycleStatus = "SUSPENDED" SkillRetired SkillLifecycleStatus = "RETIRED" @@ -202,6 +203,15 @@ type SkillProfile struct { ObservedFailureCount int64 ObservedCorrectionCount int64 RecordedAtUnixMillis int64 + // The fields below are the additive Gen2 lifecycle view. The older fields + // remain a compatibility projection; they are not another Skill registry. + Applicability ApplicabilitySpec + DependencyRefs []string + ValidationEvidenceRefs []SkillEvidenceRef + IndependentEvaluationRefs []SkillEvidenceRef + KnownFailureConditions []string + VerifiedOutcomeCount int64 + MustNotWorsenSatisfied bool } type SkillOutcomeObservation struct { diff --git a/database/migrations/054_experience_skill_reflex_learning_lifecycle.sql b/database/migrations/054_experience_skill_reflex_learning_lifecycle.sql new file mode 100644 index 0000000..28c5e46 --- /dev/null +++ b/database/migrations/054_experience_skill_reflex_learning_lifecycle.sql @@ -0,0 +1,79 @@ +BEGIN; + +SET ROLE capability_role; + +ALTER TABLE capability.skill_profiles + ADD COLUMN IF NOT EXISTS applicability JSONB NOT NULL DEFAULT '{}'::jsonb, + ADD COLUMN IF NOT EXISTS dependency_refs TEXT[] NOT NULL DEFAULT '{}', + ADD COLUMN IF NOT EXISTS validation_evidence_refs TEXT[] NOT NULL DEFAULT '{}', + ADD COLUMN IF NOT EXISTS independent_evaluation_refs TEXT[] NOT NULL DEFAULT '{}', + ADD COLUMN IF NOT EXISTS known_failure_conditions TEXT[] NOT NULL DEFAULT '{}', + ADD COLUMN IF NOT EXISTS verified_outcome_count BIGINT NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS must_not_worsen_satisfied BOOLEAN NOT NULL DEFAULT FALSE; + +ALTER TABLE capability.skill_profile_heads DROP CONSTRAINT IF EXISTS skill_profile_heads_status_check; +ALTER TABLE capability.skill_profile_heads + ADD CONSTRAINT skill_profile_heads_status_check CHECK (status IN ('CANDIDATE','EVALUATING','VALIDATED','ACTIVE','DEGRADED','SUSPENDED','RETIRED')); + +CREATE TABLE IF NOT EXISTS capability.reflex_revisions ( + reflex_id TEXT NOT NULL, + revision BIGINT NOT NULL CHECK (revision > 0), + principal_id TEXT NOT NULL, + source_skill_id TEXT NOT NULL, + source_skill_revision BIGINT NOT NULL CHECK (source_skill_revision > 0), + kind TEXT NOT NULL, + applicability JSONB NOT NULL, + implementation_ref TEXT NOT NULL, + implementation_version TEXT NOT NULL DEFAULT '', + artifact_digest TEXT NOT NULL DEFAULT '', + dependency_refs TEXT[] NOT NULL DEFAULT '{}', + validation_evidence_refs TEXT[] NOT NULL DEFAULT '{}', + known_failure_experience_refs TEXT[] NOT NULL DEFAULT '{}', + shadow_policy JSONB NOT NULL DEFAULT '{}'::jsonb, + revalidation_policy JSONB NOT NULL DEFAULT '{}'::jsonb, + last_validated_at BIGINT NOT NULL DEFAULT 0, + lifecycle TEXT NOT NULL, + supersedes_revision BIGINT NOT NULL DEFAULT 0, + execution_count BIGINT NOT NULL DEFAULT 0, + failure_count BIGINT NOT NULL DEFAULT 0, + divergence_count BIGINT NOT NULL DEFAULT 0, + recorded_at BIGINT NOT NULL CHECK (recorded_at > 0), + PRIMARY KEY (reflex_id, revision), + CHECK (lifecycle IN ('CANDIDATE','VALIDATING','ACTIVE','DEGRADED','STALE','REVALIDATING','SUSPENDED','RETIRED')) +); +CREATE TABLE IF NOT EXISTS capability.reflex_heads ( + reflex_id TEXT PRIMARY KEY, + revision BIGINT NOT NULL, + lifecycle TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +GRANT SELECT, INSERT, UPDATE ON capability.reflex_revisions, capability.reflex_heads TO capability_role; + +RESET ROLE; +SET ROLE self_role; + +CREATE TABLE IF NOT EXISTS self.improvement_needs ( + improvement_need_id TEXT PRIMARY KEY, + principal_id TEXT NOT NULL, + subject_ref JSONB NOT NULL, + problem_summary TEXT NOT NULL, + evidence_refs JSONB NOT NULL, + expected_benefit TEXT NOT NULL, + priority INTEGER NOT NULL, + state TEXT NOT NULL, + created_at_unix_millis BIGINT NOT NULL +); +ALTER TABLE self.learning_ledger + ADD COLUMN IF NOT EXISTS improvement_need_ref JSONB, + ADD COLUMN IF NOT EXISTS proposed_change_ref JSONB, + ADD COLUMN IF NOT EXISTS expected_improvement TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS evaluation_refs JSONB NOT NULL DEFAULT '[]'::jsonb, + ADD COLUMN IF NOT EXISTS development_gate_ref JSONB, + ADD COLUMN IF NOT EXISTS before_version_refs JSONB NOT NULL DEFAULT '[]'::jsonb, + ADD COLUMN IF NOT EXISTS after_version_refs JSONB NOT NULL DEFAULT '[]'::jsonb, + ADD COLUMN IF NOT EXISTS side_effects JSONB NOT NULL DEFAULT '[]'::jsonb, + ADD COLUMN IF NOT EXISTS rollback_ref JSONB; +GRANT SELECT, INSERT, UPDATE ON self.improvement_needs, self.learning_ledger TO self_role; + +RESET ROLE; +COMMIT; diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 23d0368..4dce797 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1 +1 @@ -{"task_file":"dev/tasks/GEN2-EVOLVE-002-iterative-retrieval-and-cognitive-memory-continuation.json","task_id":"GEN2-EVOLVE-002","task_revision":2} +{"task_file":"dev/tasks/GEN2-EVOLVE-003-experience-skill-reflex-learning-lifecycle.json","task_id":"GEN2-EVOLVE-003","task_revision":3} diff --git a/dev/tasks/GEN2-EVOLVE-003-experience-skill-reflex-learning-lifecycle.json b/dev/tasks/GEN2-EVOLVE-003-experience-skill-reflex-learning-lifecycle.json new file mode 100644 index 0000000..b8e7dd9 --- /dev/null +++ b/dev/tasks/GEN2-EVOLVE-003-experience-skill-reflex-learning-lifecycle.json @@ -0,0 +1,39 @@ +{ + "task_id": "GEN2-EVOLVE-003", + "task_revision": 3, + "architecture_revision": "ARCH-GEN2-000", + "base_repo_revision": "gen2-integration", + "risk_tier": "E", + "intent": "Implement the bounded evidence-governed Experience to Skill to Reflex learning lifecycle using existing Epistemics, Capability and SELF owners: candidate, independent validation, applicable routing, bounded shadow/revalidation, downgrade/retirement and Learning Ledger evidence, without autonomous production mutation.", + "requirement_ids": ["REQ-GEN2-EVOLVE-003-EXPERIENCE-SKILL-REFLEX"], + "allowed_change_scope": { + "modules": ["capability", "self", "core-runtime learning", "Gen2 evidence and governance"], + "paths": [ + "contracts/capability/skill_v2.go", "contracts/capability/reflex_v2.go", "contracts/capability/resolution.go", "contracts/capability/intelligence.go", "contracts/self/types.go", + "modules/capability/domain/skill.go", "modules/capability/domain/skill_test.go", "modules/capability/domain/reflex.go", "modules/capability/domain/reflex_test.go", "modules/capability/domain/intelligence_test.go", + "modules/self/domain/self.go", "modules/self/domain/self_test.go", + "adapters/postgres/capability_repository.go", "adapters/postgres/self_repository.go", "adapters/postgres/capability_repository_test.go", "adapters/postgres/self_repository_test.go", + "database/migrations/054_experience_skill_reflex_learning_lifecycle.sql", + "cmd/core-runtime/learning.go", "cmd/core-runtime/learning_test.go", + "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-003-experience-skill-reflex-learning-lifecycle.json", "docs/evidence/**", "docs/PROJECT_STATUS.md" + ] + }, + "forbidden_change_scope": { + "paths": ["architecture/**", "security/**", "go.mod", "go.sum", "deploy/**", "cmd/creator-web/**", "cmd/cognitive-worker/**", "adapters/models/**", "modules/epistemics/**", "adapters/postgres/epistemics_repository.go", "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": 1}, + "contracts": { + "consume": ["existing Epistemics ExperienceRecord/ReflectionCandidate", "existing Capability Skill V2/Reflex V2", "existing SELF LearningLedger/ImprovementNeed", "existing Intelligence Strategy"], + "may_change": ["Capability and SELF typed lifecycle/evidence fields necessary for bounded candidate, validation, routing and revalidation semantics only"] + }, + "semantic_owner": "Epistemics remains canonical owner of Experience and Outcome evidence; Capability remains sole canonical owner of Skill and Reflex lifecycle; SELF remains canonical owner of ImprovementNeed, competence/weakness assessments and Learning Ledger. Core Runtime only records evidence through owner repositories and never self-promotes production behavior.", + "canonical_state": ["Experience/Outcome evidence is referenced, never copied as a new source of truth", "Capability writes Skill/Reflex revisions and heads; SELF writes Learning Ledger and ImprovementNeed", "candidate, validation, shadow and revalidation never grant independent production mutation authority", "no new Memory owner, parallel Skill registry or cross-owner SQL join"], + "side_effects_and_privileges": ["No external mutation may bypass Authority -> Commit -> Executor -> verified Outcome.", "SELF may observe, evaluate and propose only; production promotion remains behind the existing governed development gates and independent evaluation.", "No provider invocation, privilege expansion, UI/browser change or V1 retirement is authorized."], + "persistence_and_migrations": ["054 is the next verified global migration number after 053; it is additive and uses existing capability_role and self_role only", "revision 3 sets devctl's database-surface compatibility delta to one solely because devctl requires it for any migration; no database owner role is created", "persist Skill/Reflex revision/head and SELF learning evidence needed for restart-safe lifecycle state; existing historical records remain untouched", "rollback disables/refuses non-active Reflex selection and returns to existing Skill/reasoning path without deleting evidence"], + "state_transitions": ["verified positive Experience with a bounded pattern and provenance -> Candidate Skill", "independent validation/replay evidence and must-not-worsen criteria -> Validated Skill -> Active eligible routing", "frequent stable Active Skill with deterministic bounded implementation and verifier -> Candidate Reflex -> Validated/Active Reflex", "dependency change, stale evidence, correction, failure trend or shadow divergence -> Stale/Suspicious -> revalidation request -> Active, rewrite proposal or Retired", "Reflex decline/failure -> Skill -> reasoning; no execution can bypass Authority/Commit/Executor"], + "failure_retry_restart": ["one success never promotes Candidate directly to Active Skill or Reflex", "shadow is sampled/bounded and never delays ordinary low-risk response; divergence records evidence and ImprovementNeed rather than rewriting Reflex", "revalidation is bounded and never retries endlessly; real failures remain fail-closed and escalate", "durable revisions, validation evidence and Ledger survive restart; no direct provider invocation or process-local canonical state"], + "assumptions": ["task branch starts at gen2-integration a07aba0", "UI/browser acceptance remains deferred by owner; V1 compatibility remains retained", "a verified inspection found 054 free, existing SkillProfile persistence, Reflex V2 contract without durable lifecycle persistence, and SELF typed ledger without a repository", "revision 2 adds only contracts/capability/resolution.go because the existing canonical SkillProfile is declared there and its durable evidence fields must be additive rather than creating a parallel Skill registry", "revision 3 satisfies devctl's migration compatibility gate without creating any role or privilege", "automatic arbitrary production code generation is not in scope; candidate executable strategies must use an existing bounded implementation reference and governed development remains the only production-promotion route"], + "acceptance_scenarios": ["EVOLVE-003-SCN-01 verified outcome and pattern create Candidate Skill with source Experience/Outcome references", "EVOLVE-003-SCN-02 independent replay/evaluation evidence promotes Skill only after repeatability and must-not-worsen checks", "EVOLVE-003-SCN-03 applicable fresh Active Skill is routed; inapplicable, stale or weak Skill escalates to reasoning", "EVOLVE-003-SCN-04 sufficient stable evidence creates Candidate Reflex; verified bounded Reflex becomes Active and retains fallback", "EVOLVE-003-SCN-05 dependency change, correction, failure trend and stale validation trigger bounded revalidation/downgrade rather than silent use", "EVOLVE-003-SCN-06 sampled shadow divergence records ImprovementNeed/LearningLedger evidence and does not rewrite production Reflex", "EVOLVE-003-SCN-07 user correction links the affected Skill/Reflex revision and preserves conditional known failure evidence", "EVOLVE-003-SCN-08 retired Reflex is excluded from routing while all evidence/history remains queryable"], + "required_tests": ["FAST: targeted go test and go vet for changed capability, self, core-runtime and postgres packages", "FAST: devctl preflight, check-diff, independent governance, architecture check, git diff --check", "MILESTONE: focused lifecycle/persistence/restart and escalation tests only; no browser, production canary or FULL suite"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"] +} diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 6f718c4..a0ab021 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -2,6 +2,8 @@ ## Gen2 M6 checkpoint — 2026-08-30 +- `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. - `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. diff --git a/docs/evidence/gen2-evolve-003-milestone-2026-08-30.md b/docs/evidence/gen2-evolve-003-milestone-2026-08-30.md new file mode 100644 index 0000000..ac7965f --- /dev/null +++ b/docs/evidence/gen2-evolve-003-milestone-2026-08-30.md @@ -0,0 +1,16 @@ +# GEN2-EVOLVE-003 — Experience to Skill to Reflex Learning Lifecycle + +- Task/revision: `GEN2-EVOLVE-003 r3`; base `a07aba0`. +- Canonical owners remain unchanged: Epistemics owns Experience/Outcome evidence; Capability owns the single SkillProfile lifecycle and Reflex revisions; SELF owns ImprovementNeed and Learning Ledger. Evidence is referenced by stable refs, not copied into another memory or Skill registry. +- Skill lifecycle: verified positive evidence produces `CANDIDATE`; at least two verified outcomes plus an independent evaluation and must-not-worsen evidence are required for `VALIDATED`; a separate explicit transition makes it `ACTIVE`. +- Routing: an ineligible Reflex falls through to an eligible Skill and then Local reasoning. A degraded, stale, suspended, retired or insufficient-confidence Skill cannot be fast-applied. +- Reflex lifecycle: only an Active Skill with three verified outcomes and a bounded deterministic/rule/workflow implementation can become a Candidate. Independent evaluation promotes it to Active. Sampled shadow divergence marks it `STALE` and creates only a SELF revalidation proposal; it cannot rewrite or promote production code. +- Revalidation triggers cover stale lifecycle, dependency/version change, correction count and validation age. Retired Reflex revisions remain durable history and cannot be selected. +- Persistence: additive migration `054` extends the existing Capability SkillProfile record, adds Capability-owned Reflex revision/head tables, and adds SELF ImprovementNeed/Learning Ledger evidence fields using existing roles. Rollback excludes a non-Active Reflex and falls back to Skill/reasoning; no evidence is deleted. +- Gap intentionally retained: automatic generation of executable production Reflex code is not implemented. It requires a separately authorized governed-development task with independent evaluation and promotion gates. + +## Verification + +MILESTONE: focused `go test`/`go vet` for Capability domain, SELF domain, PostgreSQL adapter, Core Runtime and Capability contracts; `devctl preflight`, `check-diff`, `independent`, `check-architecture`, migration consistency and `git diff --check`. + +Focused regressions cover no one-run promotion, independent validation, Reflex candidate requirements, shadow divergence/revalidation rather than rewrite, Skill-to-reasoning escalation, SELF proposal-only behavior, and historical evidence preservation. diff --git a/modules/capability/domain/intelligence_test.go b/modules/capability/domain/intelligence_test.go index 2847aa1..ac37f9a 100644 --- a/modules/capability/domain/intelligence_test.go +++ b/modules/capability/domain/intelligence_test.go @@ -13,3 +13,13 @@ func TestStrategyUsesRiskAndCostSignals(t *testing.T) { t.Fatalf("%#v", got) } } + +func TestUnavailableReflexFallsBackThroughSkillToReasoning(t *testing.T) { + request := capability.StrategyRequest{PreferLowLatency: true, RequiredQuality: 700} + if got := request.SelectEligible(capability.StrategyEligibility{SkillEligible: true}); got.Path != capability.PathSkill { + t.Fatalf("expected Skill fallback, got %#v", got) + } + if got := request.SelectEligible(capability.StrategyEligibility{}); got.Path != capability.PathSmallLocal || !got.Escalate { + t.Fatalf("expected reasoning fallback, got %#v", got) + } +} diff --git a/modules/capability/domain/reflex.go b/modules/capability/domain/reflex.go index ce637ba..344d7f9 100644 --- a/modules/capability/domain/reflex.go +++ b/modules/capability/domain/reflex.go @@ -1,19 +1,91 @@ package domain import ( + "errors" "fmt" "git.cin.su/m/Assistant/contracts/capability" ) +var ErrInsufficientReflexEvidence = errors.New("insufficient reflex evidence") + func ValidateReflexRevision(v capability.ReflexRevision) error { if v.ReflexID == "" || v.SourceSkillRevision < 1 || v.Kind == "" || v.ImplementationRef == "" || v.Lifecycle == "" || v.RecordedAt < 1 { return fmt.Errorf("invalid reflex revision") } - if v.Lifecycle == capability.ReflexActive && len(v.ValidationEvidenceRefs) == 0 { + if (v.Lifecycle == capability.ReflexActive || v.Lifecycle == capability.ReflexValidating) && len(v.ValidationEvidenceRefs) == 0 { return fmt.Errorf("active reflex requires evidence") } return nil } + +// CandidateReflex admits only a bounded, deterministic implementation from an +// already active Skill with multiple verified results. It never activates it. +func CandidateReflex(skill capability.SkillProfile, value capability.ReflexRevision, verifierEvidence capability.SkillEvidenceRef, recordedAt int64) (capability.ReflexRevision, error) { + if err := ValidateSkillProfile(skill); err != nil || skill.Status != capability.SkillActive || skill.VerifiedOutcomeCount < 3 || verifierEvidence == "" || recordedAt < 1 { + return capability.ReflexRevision{}, ErrInsufficientReflexEvidence + } + if value.Kind != capability.ReflexDeterministicCode && value.Kind != capability.ReflexRule && value.Kind != capability.ReflexWorkflow && value.Kind != capability.ReflexBoundedAdapter { + return capability.ReflexRevision{}, ErrInsufficientReflexEvidence + } + value.SourceSkillRevision = skill.Revision + value.Lifecycle = capability.ReflexCandidate + value.ValidationEvidenceRefs = appendEvidence(value.ValidationEvidenceRefs, verifierEvidence) + value.LastValidatedAt = 0 + value.RecordedAt = recordedAt + if err := ValidateReflexRevision(value); err != nil { + return capability.ReflexRevision{}, err + } + return value, nil +} + +func ValidateAndActivateReflex(value capability.ReflexRevision, independentEvidence capability.SkillEvidenceRef, mustNotWorsen bool, recordedAt int64) (capability.ReflexRevision, error) { + if err := ValidateReflexRevision(value); err != nil || value.Lifecycle != capability.ReflexCandidate || independentEvidence == "" || !mustNotWorsen || recordedAt < 1 { + return capability.ReflexRevision{}, ErrInsufficientReflexEvidence + } + next := value + next.Revision++ + next.Lifecycle = capability.ReflexActive + next.ValidationEvidenceRefs = appendEvidence(next.ValidationEvidenceRefs, independentEvidence) + next.LastValidatedAt = recordedAt + next.RecordedAt = recordedAt + return next, nil +} + +// ObserveShadow keeps normal execution non-blocking. Divergence marks the +// revision stale for governed revalidation; it cannot replace implementation. +func ObserveShadow(value capability.ReflexRevision, comparison capability.ShadowComparison) (capability.ReflexRevision, bool, error) { + if err := ValidateReflexRevision(value); err != nil || comparison.ReflexID != value.ReflexID || comparison.ReflexRevision != value.Revision || comparison.ObservedAt < 1 { + return capability.ReflexRevision{}, false, ErrInsufficientReflexEvidence + } + next := value + next.ExecutionCount++ + if comparison.Diverged || !comparison.VerifierPassed { + next.DivergenceCount++ + next.Lifecycle = capability.ReflexStale + next.RecordedAt = comparison.ObservedAt + return next, true, nil + } + return next, false, nil +} + +func RevalidationRequired(value capability.ReflexRevision, dependencyChanged bool, correctionCount int64, now int64) bool { + if value.Lifecycle == capability.ReflexRetired || value.Lifecycle == capability.ReflexStale || value.Lifecycle == capability.ReflexDegraded { + return true + } + if dependencyChanged || (value.Revalidation.RevalidateAfterCorrectionCount != nil && correctionCount >= *value.Revalidation.RevalidateAfterCorrectionCount) { + return true + } + return value.Revalidation.MaxAgeMillis != nil && value.LastValidatedAt > 0 && now-value.LastValidatedAt >= *value.Revalidation.MaxAgeMillis +} + +func appendEvidence(values []capability.SkillEvidenceRef, value capability.SkillEvidenceRef) []capability.SkillEvidenceRef { + for _, existing := range values { + if existing == value { + return values + } + } + return append(values, value) +} func ReflexFallback(d capability.FastPathDecision) string { if d.Decision == capability.FastPathApplied { return "reflex" diff --git a/modules/capability/domain/reflex_test.go b/modules/capability/domain/reflex_test.go index ad9aa98..fab6e40 100644 --- a/modules/capability/domain/reflex_test.go +++ b/modules/capability/domain/reflex_test.go @@ -19,3 +19,19 @@ func TestReflexActiveRequiresEvidenceAndFallbacks(t *testing.T) { t.Fatal("missing escalation") } } + +func TestReflexRequiresStableSkillAndShadowDivergenceOnlyRequestsReview(t *testing.T) { + skill := capability.SkillProfile{ID: "s", Revision: 4, Name: "bounded", Description: "bounded", Status: capability.SkillActive, ConfidenceMillis: 900, RecordedAtUnixMillis: 1, VerifiedOutcomeCount: 3} + candidate, err := domain.CandidateReflex(skill, capability.ReflexRevision{ReflexID: "r", Revision: 1, Kind: capability.ReflexRule, ImplementationRef: "rule", RecordedAt: 1}, "verifier:success", 2) + if err != nil || candidate.Lifecycle != capability.ReflexCandidate { + t.Fatalf("candidate=%#v err=%v", candidate, err) + } + active, err := domain.ValidateAndActivateReflex(candidate, "evaluation:independent", true, 3) + if err != nil || active.Lifecycle != capability.ReflexActive { + t.Fatalf("active=%#v err=%v", active, err) + } + next, review, err := domain.ObserveShadow(active, capability.ShadowComparison{ReflexID: "r", ReflexRevision: active.Revision, EvidenceRefs: []capability.SkillEvidenceRef{"shadow:1"}, VerifierPassed: false, Diverged: true, ObservedAt: 4}) + if err != nil || !review || next.Lifecycle != capability.ReflexStale { + t.Fatalf("next=%#v review=%t err=%v", next, review, err) + } +} diff --git a/modules/capability/domain/skill.go b/modules/capability/domain/skill.go index f801b75..6647699 100644 --- a/modules/capability/domain/skill.go +++ b/modules/capability/domain/skill.go @@ -9,6 +9,7 @@ import ( var ErrInvalidSkillProfile = errors.New("invalid skill profile") var ErrInvalidFastPathInput = errors.New("invalid fast path input") +var ErrInsufficientSkillEvidence = errors.New("insufficient independent skill evidence") func ValidateSkillProfile(value capability.SkillProfile) error { if value.ID == "" || value.Revision < 1 || strings.TrimSpace(value.Name) == "" || @@ -17,13 +18,59 @@ func ValidateSkillProfile(value capability.SkillProfile) error { return ErrInvalidSkillProfile } switch value.Status { - case capability.SkillCandidate, capability.SkillEvaluating, capability.SkillValidated, capability.SkillDegraded, capability.SkillSuspended, capability.SkillRetired: + case capability.SkillCandidate, capability.SkillEvaluating, capability.SkillValidated, capability.SkillActive, capability.SkillDegraded, capability.SkillSuspended, capability.SkillRetired: default: return ErrInvalidSkillProfile } return nil } +// CandidateFromVerifiedExperience starts a reusable approach but deliberately +// does not validate or activate it. Evidence stays in Epistemics by reference. +func CandidateFromVerifiedExperience(profile capability.SkillProfile, experience capability.SkillEvidenceRef, verified bool, recordedAt int64) (capability.SkillProfile, error) { + if err := ValidateSkillProfile(profile); err != nil || experience == "" || !verified || recordedAt < 1 { + return capability.SkillProfile{}, ErrInvalidSkillProfile + } + next := profile + next.Revision++ + next.Status = capability.SkillCandidate + next.SupportingExperienceRefs = appendEvidenceRef(next.SupportingExperienceRefs, experience) + next.VerifiedOutcomeCount++ + next.RecordedAtUnixMillis = recordedAt + return next, nil +} + +// ValidateSkill requires repeatable verified outcomes plus evidence produced by +// an evaluator other than the candidate creator. It never activates a Skill. +func ValidateSkill(profile capability.SkillProfile, evaluation capability.SkillEvidenceRef, independent bool, mustNotWorsen bool, recordedAt int64) (capability.SkillProfile, error) { + if err := ValidateSkillProfile(profile); err != nil || evaluation == "" || !independent || !mustNotWorsen || recordedAt < 1 { + return capability.SkillProfile{}, ErrInsufficientSkillEvidence + } + if profile.VerifiedOutcomeCount < 2 || len(profile.SupportingExperienceRefs) < 2 { + return capability.SkillProfile{}, ErrInsufficientSkillEvidence + } + next := profile + next.Revision++ + next.Status = capability.SkillValidated + next.ValidationEvidenceRefs = appendEvidenceRef(next.ValidationEvidenceRefs, evaluation) + next.IndependentEvaluationRefs = appendEvidenceRef(next.IndependentEvaluationRefs, evaluation) + next.MustNotWorsenSatisfied = true + next.LastValidatedUnixMillis = recordedAt + next.RecordedAtUnixMillis = recordedAt + return next, nil +} + +func ActivateSkill(profile capability.SkillProfile, recordedAt int64) (capability.SkillProfile, error) { + if err := ValidateSkillProfile(profile); err != nil || profile.Status != capability.SkillValidated || len(profile.IndependentEvaluationRefs) == 0 || !profile.MustNotWorsenSatisfied || recordedAt < 1 { + return capability.SkillProfile{}, ErrInsufficientSkillEvidence + } + next := profile + next.Revision++ + next.Status = capability.SkillActive + next.RecordedAtUnixMillis = recordedAt + return next, nil +} + func ObserveSkillOutcome(profile capability.SkillProfile, observation capability.SkillOutcomeObservation) (capability.SkillProfile, error) { if err := ValidateSkillProfile(profile); err != nil { return capability.SkillProfile{}, err @@ -37,10 +84,6 @@ func ObserveSkillOutcome(profile capability.SkillProfile, observation capability switch observation.Outcome { case capability.UsageSucceeded: next.SupportingExperienceRefs = appendEvidenceRef(next.SupportingExperienceRefs, observation.ExperienceRef) - if next.Status == capability.SkillCandidate || next.Status == capability.SkillEvaluating { - next.Status = capability.SkillValidated - } - next.LastValidatedUnixMillis = observation.ObservedAtUnixMillis next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis + 25) case capability.UsageFailed: next.KnownFailureExperienceRefs = appendEvidenceRef(next.KnownFailureExperienceRefs, observation.ExperienceRef) @@ -57,7 +100,7 @@ func ObserveSkillOutcome(profile capability.SkillProfile, observation capability next.KnownFailureExperienceRefs = appendEvidenceRef(next.KnownFailureExperienceRefs, observation.ExperienceRef) next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis - 200) } - if next.Status == capability.SkillValidated && (next.ObservedFailureCount >= 3 || next.ObservedCorrectionCount >= 2 || next.ConfidenceMillis < 500) { + if (next.Status == capability.SkillValidated || next.Status == capability.SkillActive) && (next.ObservedFailureCount >= 3 || next.ObservedCorrectionCount >= 2 || next.ConfidenceMillis < 500) { next.Status = capability.SkillDegraded } if next.Status == capability.SkillDegraded && next.ConfidenceMillis < 250 { @@ -80,7 +123,7 @@ func DecideFastPath(profile capability.SkillProfile, supported bool, confidenceM ObservedAtUnixMillis: observedAtUnixMillis, } switch { - case profile.Status == capability.SkillSuspended || profile.Status == capability.SkillRetired: + case profile.Status == capability.SkillSuspended || profile.Status == capability.SkillRetired || profile.Status == capability.SkillDegraded: return decline(decision, capability.FastPathStale, "skill profile is not eligible for fast-path use"), nil case environmentVersionChanged: return decline(decision, capability.FastPathStale, "environment or implementation version changed since validation"), nil @@ -90,7 +133,7 @@ func DecideFastPath(profile capability.SkillProfile, supported bool, confidenceM return decline(decision, capability.FastPathOutOfDistribution, "input is outside supported semantics"), nil case confidenceMillis < 700 || profile.ConfidenceMillis < 700: return decline(decision, capability.FastPathLowConfidence, "confidence is too low for optimized path"), nil - case profile.Status != capability.SkillValidated: + case profile.Status != capability.SkillActive: return decline(decision, capability.FastPathNotApplicable, "skill is not validated"), nil default: return decision, nil diff --git a/modules/capability/domain/skill_test.go b/modules/capability/domain/skill_test.go index 2a852a7..93365d6 100644 --- a/modules/capability/domain/skill_test.go +++ b/modules/capability/domain/skill_test.go @@ -34,7 +34,7 @@ func TestFastPathDeclinesUnsupportedInputAndEscalatesToThinking(t *testing.T) { } } -func TestValidatedSkillCanApplyFastPathWhenFreshAndConfident(t *testing.T) { +func TestActiveSkillCanApplyFastPathWhenFreshAndConfident(t *testing.T) { decision, err := DecideFastPath(skillProfile(), true, 950, false, false, 2000) if err != nil { t.Fatal(err) @@ -58,18 +58,43 @@ func TestUnknownAndSkippedOutcomesDoNotBecomeSkillFailures(t *testing.T) { } } +func TestCandidateNeedsIndependentRepeatableValidationBeforeActive(t *testing.T) { + profile := skillProfile() + profile.Status = capability.SkillCandidate + profile.VerifiedOutcomeCount = 1 + profile.SupportingExperienceRefs = []capability.SkillEvidenceRef{"experience:1"} + if _, err := ValidateSkill(profile, "evaluation:independent", true, true, 2000); err == nil { + t.Fatal("one success promoted a skill") + } + profile.VerifiedOutcomeCount = 2 + profile.SupportingExperienceRefs = append(profile.SupportingExperienceRefs, "experience:2") + validated, err := ValidateSkill(profile, "evaluation:independent", true, true, 2000) + if err != nil || validated.Status != capability.SkillValidated { + t.Fatalf("validation=%#v err=%v", validated, err) + } + active, err := ActivateSkill(validated, 2100) + if err != nil || active.Status != capability.SkillActive { + t.Fatalf("activation=%#v err=%v", active, err) + } +} + func skillProfile() capability.SkillProfile { return capability.SkillProfile{ - ID: "skill-obvious-reminder-response-v1", - Revision: 1, - Name: "Resolve obvious reminder continuation", - Description: "Use a recent unambiguous reminder/attention target for complete/snooze/cancel without cognition.", - ApplicableContext: []string{"continuity:reminder-response"}, - ImplementationRef: "continuity:human-response-target-resolver-v1", - ImplementationVersion: "1", - Status: capability.SkillValidated, - ConfidenceMillis: 850, - LastValidatedUnixMillis: 1000, - RecordedAtUnixMillis: 1000, + ID: "skill-obvious-reminder-response-v1", + Revision: 1, + Name: "Resolve obvious reminder continuation", + Description: "Use a recent unambiguous reminder/attention target for complete/snooze/cancel without cognition.", + ApplicableContext: []string{"continuity:reminder-response"}, + ImplementationRef: "continuity:human-response-target-resolver-v1", + ImplementationVersion: "1", + Status: capability.SkillActive, + ConfidenceMillis: 850, + LastValidatedUnixMillis: 1000, + RecordedAtUnixMillis: 1000, + SupportingExperienceRefs: []capability.SkillEvidenceRef{"experience:1", "experience:2"}, + IndependentEvaluationRefs: []capability.SkillEvidenceRef{"evaluation:independent"}, + ValidationEvidenceRefs: []capability.SkillEvidenceRef{"evaluation:independent"}, + VerifiedOutcomeCount: 2, + MustNotWorsenSatisfied: true, } } diff --git a/modules/self/domain/self.go b/modules/self/domain/self.go index fe32524..cd4205b 100644 --- a/modules/self/domain/self.go +++ b/modules/self/domain/self.go @@ -1,8 +1,25 @@ package domain import ( + "fmt" "git.cin.su/m/Assistant/contracts/self" + "git.cin.su/m/Assistant/platform/ids" ) 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") + } + 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} + 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 { + return self.ImprovementNeed{}, self.LearningLedgerEntry{}, err + } + return need, ledger, nil +} diff --git a/modules/self/domain/self_test.go b/modules/self/domain/self_test.go index b18175e..a83b9c2 100644 --- a/modules/self/domain/self_test.go +++ b/modules/self/domain/self_test.go @@ -16,3 +16,12 @@ func TestCompetenceCannotBeSelfDeclaredWithoutEvidence(t *testing.T) { t.Fatal(err) } } + +func TestShadowDivergenceCreatesProposalNotPromotion(t *testing.T) { + reflex := selfcontract.SemanticReference{OwnerNamespace: "capability", SemanticKind: "reflex", StableID: "r"} + evidence := []selfcontract.SemanticReference{{OwnerNamespace: "epistemics", SemanticKind: "experience", StableID: "e"}} + need, ledger, err := domain.ImprovementForReflexDivergence("p", reflex, evidence, 1) + if err != nil || need.State != "OPEN" || ledger.Decision != "PROPOSE_REVALIDATION" { + t.Fatalf("need=%#v ledger=%#v err=%v", need, ledger, err) + } +}