package domain import ( "errors" "strings" "git.cin.su/m/Assistant/contracts/capability" ) 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) == "" || strings.TrimSpace(value.Description) == "" || value.Status == "" || value.ConfidenceMillis < 0 || value.ConfidenceMillis > 1000 || value.RecordedAtUnixMillis < 1 { return ErrInvalidSkillProfile } switch value.Status { 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 } if observation.ExperienceRef == "" || observation.ObservedAtUnixMillis < 1 { return capability.SkillProfile{}, ErrInvalidSkillProfile } next := profile next.Revision++ next.RecordedAtUnixMillis = observation.ObservedAtUnixMillis switch observation.Outcome { case capability.UsageSucceeded: next.SupportingExperienceRefs = appendEvidenceRef(next.SupportingExperienceRefs, observation.ExperienceRef) next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis + 25) case capability.UsageFailed: next.KnownFailureExperienceRefs = appendEvidenceRef(next.KnownFailureExperienceRefs, observation.ExperienceRef) next.ObservedFailureCount++ next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis - 150) case capability.UsageUnknown, capability.UsageSkipped: // UNKNOWN and SKIPPED are evidence of incomplete observation or a safe // non-application, not evidence that the skill itself failed. default: return capability.SkillProfile{}, ErrInvalidSkillProfile } if observation.UserCorrected { next.ObservedCorrectionCount++ next.KnownFailureExperienceRefs = appendEvidenceRef(next.KnownFailureExperienceRefs, observation.ExperienceRef) next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis - 200) } 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 { next.Status = capability.SkillSuspended } return next, nil } func DecideFastPath(profile capability.SkillProfile, supported bool, confidenceMillis int64, environmentVersionChanged bool, conflict bool, observedAtUnixMillis int64) (capability.FastPathDecision, error) { if err := ValidateSkillProfile(profile); err != nil { return capability.FastPathDecision{}, err } if confidenceMillis < 0 || confidenceMillis > 1000 || observedAtUnixMillis < 1 { return capability.FastPathDecision{}, ErrInvalidFastPathInput } decision := capability.FastPathDecision{ SkillProfileID: profile.ID, Decision: capability.FastPathApplied, Explanation: "validated fast path applied", ObservedAtUnixMillis: observedAtUnixMillis, } switch { 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 case conflict: return decline(decision, capability.FastPathConflict, "input conflicts with known fast-path assumptions"), nil case !supported: 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.SkillActive: return decline(decision, capability.FastPathNotApplicable, "skill is not validated"), nil default: return decision, nil } } func decline(decision capability.FastPathDecision, reason capability.FastPathDeclineReason, explanation string) capability.FastPathDecision { decision.Decision = capability.FastPathEscalate decision.DeclineReason = reason decision.RequiresCognition = true decision.Explanation = explanation return decision } func appendEvidenceRef(values []capability.SkillEvidenceRef, ref capability.SkillEvidenceRef) []capability.SkillEvidenceRef { for _, value := range values { if value == ref { return values } } return append(values, ref) } func clampConfidence(value int64) int64 { if value < 0 { return 0 } if value > 1000 { return 1000 } return value }