208 lines
5.5 KiB
Go
208 lines
5.5 KiB
Go
package workflows
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
var ErrNotImplemented = errors.New("workflow runner not implemented")
|
|
|
|
type DiagnosticInput struct {
|
|
UserID string
|
|
Track string
|
|
TargetRole string
|
|
Stack []string
|
|
}
|
|
|
|
type Runner interface {
|
|
DiagnoseJobSeeker(context.Context, DiagnosticInput) (DiagnosticResult, error)
|
|
GradeInterviewAnswer(context.Context, GradeAnswerInput) (GradedAnswer, error)
|
|
ExtractLearningMemory(context.Context, GradedAnswer) (MemoryUpdateCandidate, error)
|
|
SelectNextChallenge(context.Context, NextChallengeInput) (NextChallenge, error)
|
|
UpdateReadinessMap(context.Context, ReadinessUpdateInput) (ReadinessUpdate, error)
|
|
}
|
|
|
|
type GradeAnswerInput struct {
|
|
UserID string
|
|
QuestionID string
|
|
AnswerID string
|
|
AnswerText string
|
|
Concepts []ConceptRef
|
|
}
|
|
|
|
type NextChallengeInput struct {
|
|
UserID string
|
|
Track string
|
|
}
|
|
|
|
type ReadinessUpdateInput struct {
|
|
UserID string
|
|
Track string
|
|
}
|
|
|
|
type StubRunner struct{}
|
|
|
|
func NewStubRunner() StubRunner {
|
|
return StubRunner{}
|
|
}
|
|
|
|
func (StubRunner) DiagnoseJobSeeker(context.Context, DiagnosticInput) (DiagnosticResult, error) {
|
|
return DiagnosticResult{}, ErrNotImplemented
|
|
}
|
|
|
|
func (StubRunner) GradeInterviewAnswer(_ context.Context, input GradeAnswerInput) (GradedAnswer, error) {
|
|
wordCount := len(strings.Fields(input.AnswerText))
|
|
overall := AnswerPartial
|
|
if wordCount >= 18 {
|
|
overall = AnswerSolid
|
|
}
|
|
if wordCount < 8 {
|
|
overall = AnswerMiss
|
|
}
|
|
|
|
grade := GradedAnswer{
|
|
UserID: input.UserID,
|
|
AnswerID: input.AnswerID,
|
|
QuestionID: input.QuestionID,
|
|
Concepts: append([]ConceptRef(nil), input.Concepts...),
|
|
Scores: AnswerScores{
|
|
Correctness: scoreFromWords(wordCount, 8),
|
|
Depth: scoreFromWords(wordCount, 14),
|
|
Communication: scoreFromWords(wordCount, 10),
|
|
ProductionJudgment: scoreFromWords(wordCount, 20),
|
|
},
|
|
Overall: overall,
|
|
Strengths: []string{"Answer was captured and evaluated through the typed workflow boundary."},
|
|
Gaps: []string{},
|
|
Evidence: []EvidenceRef{
|
|
{
|
|
Kind: EvidenceAnswer,
|
|
ID: input.AnswerID,
|
|
Quote: input.AnswerText,
|
|
Confidence: 1,
|
|
},
|
|
},
|
|
FollowUp: FollowUpRecommendation{},
|
|
}
|
|
|
|
if overall == AnswerMiss || overall == AnswerPartial {
|
|
grade.Gaps = []string{"Answer needs more concrete reasoning and tradeoff discussion."}
|
|
grade.FollowUp = FollowUpRecommendation{
|
|
Needed: true,
|
|
Question: "Can you give a concrete production example and explain the tradeoff?",
|
|
Purpose: FollowUpRepair,
|
|
}
|
|
}
|
|
return grade, nil
|
|
}
|
|
|
|
func (StubRunner) ExtractLearningMemory(_ context.Context, grade GradedAnswer) (MemoryUpdateCandidate, error) {
|
|
candidate := MemoryUpdateCandidate{
|
|
UserID: grade.UserID,
|
|
SourceAnswerID: grade.AnswerID,
|
|
Updates: []MemoryUpdate{},
|
|
}
|
|
if len(grade.Evidence) == 0 {
|
|
return candidate, nil
|
|
}
|
|
|
|
state := readinessFromOverall(grade.Overall)
|
|
durability := DurabilityTentative
|
|
if grade.Overall == AnswerStrong {
|
|
durability = DurabilityConfirmed
|
|
}
|
|
|
|
for _, concept := range grade.Concepts {
|
|
candidate.Updates = append(candidate.Updates, MemoryUpdate{
|
|
Kind: MemoryConceptMastery,
|
|
Concept: concept,
|
|
ProposedState: state,
|
|
Summary: "Concept readiness updated from diagnostic interview answer.",
|
|
Evidence: append([]EvidenceRef(nil), grade.Evidence...),
|
|
Confidence: confidenceFromOverall(grade.Overall),
|
|
Durability: durability,
|
|
})
|
|
if grade.FollowUp.Needed {
|
|
candidate.Updates = append(candidate.Updates,
|
|
MemoryUpdate{
|
|
Kind: MemoryMisconception,
|
|
Concept: concept,
|
|
ProposedState: ReadinessFragile,
|
|
Summary: "Needs more concrete reasoning and tradeoff discussion.",
|
|
Evidence: append([]EvidenceRef(nil), grade.Evidence...),
|
|
Confidence: 0.62,
|
|
Durability: DurabilityTentative,
|
|
},
|
|
MemoryUpdate{
|
|
Kind: MemoryIntervention,
|
|
Concept: concept,
|
|
ProposedState: state,
|
|
Summary: grade.FollowUp.Question,
|
|
Evidence: append([]EvidenceRef(nil), grade.Evidence...),
|
|
Confidence: 0.7,
|
|
Durability: DurabilityTentative,
|
|
},
|
|
MemoryUpdate{
|
|
Kind: MemoryReviewSchedule,
|
|
Concept: concept,
|
|
ProposedState: state,
|
|
Summary: "Review with a concrete production example before raising difficulty.",
|
|
Evidence: append([]EvidenceRef(nil), grade.Evidence...),
|
|
Confidence: 0.7,
|
|
Durability: DurabilityTentative,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
return candidate, nil
|
|
}
|
|
|
|
func (StubRunner) SelectNextChallenge(context.Context, NextChallengeInput) (NextChallenge, error) {
|
|
return NextChallenge{}, ErrNotImplemented
|
|
}
|
|
|
|
func (StubRunner) UpdateReadinessMap(context.Context, ReadinessUpdateInput) (ReadinessUpdate, error) {
|
|
return ReadinessUpdate{}, ErrNotImplemented
|
|
}
|
|
|
|
func scoreFromWords(wordCount int, target int) int {
|
|
if wordCount >= target {
|
|
return 4
|
|
}
|
|
if wordCount >= target/2 {
|
|
return 2
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func readinessFromOverall(overall AnswerOverall) ReadinessState {
|
|
switch overall {
|
|
case AnswerMiss:
|
|
return ReadinessFragile
|
|
case AnswerPartial:
|
|
return ReadinessImproving
|
|
case AnswerSolid:
|
|
return ReadinessInterviewReady
|
|
case AnswerStrong:
|
|
return ReadinessStrongSignal
|
|
default:
|
|
return ReadinessUnknown
|
|
}
|
|
}
|
|
|
|
func confidenceFromOverall(overall AnswerOverall) float64 {
|
|
switch overall {
|
|
case AnswerMiss:
|
|
return 0.58
|
|
case AnswerPartial:
|
|
return 0.68
|
|
case AnswerSolid:
|
|
return 0.82
|
|
case AnswerStrong:
|
|
return 0.9
|
|
default:
|
|
return 0.5
|
|
}
|
|
}
|