Files

208 lines
5.5 KiB
Go
Raw Permalink Normal View History

2026-04-26 16:14:31 +09:00
package workflows
import (
"context"
"errors"
2026-04-26 16:24:35 +09:00
"strings"
2026-04-26 16:14:31 +09:00
)
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
2026-04-26 16:24:35 +09:00
Concepts []ConceptRef
2026-04-26 16:14:31 +09:00
}
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
}
2026-04-26 16:24:35 +09:00
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{
2026-04-26 16:34:52 +09:00
UserID: input.UserID,
2026-04-26 16:24:35 +09:00
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
2026-04-26 16:14:31 +09:00
}
2026-04-26 16:34:52 +09:00
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
2026-04-26 16:14:31 +09:00
}
func (StubRunner) SelectNextChallenge(context.Context, NextChallengeInput) (NextChallenge, error) {
return NextChallenge{}, ErrNotImplemented
}
func (StubRunner) UpdateReadinessMap(context.Context, ReadinessUpdateInput) (ReadinessUpdate, error) {
return ReadinessUpdate{}, ErrNotImplemented
}
2026-04-26 16:24:35 +09:00
func scoreFromWords(wordCount int, target int) int {
if wordCount >= target {
return 4
}
if wordCount >= target/2 {
return 2
}
return 1
}
2026-04-26 16:34:52 +09:00
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
}
}