Files

169 lines
7.4 KiB
Go

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
}