Files

126 lines
5.1 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")
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.SkillDegraded, capability.SkillSuspended, capability.SkillRetired:
default:
return ErrInvalidSkillProfile
}
return 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)
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)
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.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:
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.SkillValidated:
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
}