Documentation
¶
Overview ¶
Package app — export_pack.go implements the export use case.
Exports all questions for a given topic from the database back to the canonical pack file format (YAML or JSON). Questions are sorted deterministically by created_at ASC, then by hash for tie-breaking, ensuring byte-stable output for the same data.
Package app contains application use cases for golearn.
Package app — selection_mode.go defines the question selection modes.
Modes control how questions are chosen for a practice session:
- Balanced: unseen → weak → random fill (default, formerly "practice")
- ByDifficulty: filter to a chosen difficulty bucket, then apply Balanced logic
- Weakest: focus on weakest tag or weakest questions
Package app — selector.go implements the question selection policy.
Strategy (MVP):
- Unseen questions (zero prior attempts) — shuffled
- Weak questions (highest wrong rate) — sorted descending
- Random fill from remaining — shuffled
No duplicates within a session. Capped at n. Uses a supplied random source for test determinism.
Package app — selector_difficulty.go implements the By Difficulty selection mode.
Filters candidate questions to the chosen difficulty bucket, then applies the Balanced selector logic (unseen → weak → random fill) within that set.
Package app — selector_weakest.go implements the Weakest selection mode.
Two sub-modes:
- Weakest by Tag: find the tag with lowest accuracy, select questions from that tag using Balanced logic within the tag set.
- Weakest by Questions: rank all questions by wrong rate and select the weakest ones directly. Falls back to Balanced fill if not enough.
Package app — session.go implements the session lifecycle use cases: StartSession, GetNextSessionQuestion, RecordAttempt, EndSession.
The SessionEngine holds in-memory state for the current session's selected question queue and cursor position.
Index ¶
- Constants
- Variables
- func EnsureDefaultUser(userRepo ports.UserRepository) ([]domain.User, error)
- func ModeDisplayName(m SelectionMode) string
- func ModeLabel(mode SelectionMode, params ModeParams) string
- func ModeParamsJSON(p ModeParams) string
- func ResolveUser(userRepo ports.UserRepository, configuredUserID int64) (*domain.User, bool, error)
- func SelectByDifficulty(questions []domain.Question, stats map[int64]ports.QuestionStats, n int, ...) (selected []domain.Question, note string)
- func SelectQuestions(questions []domain.Question, stats map[int64]ports.QuestionStats, n int, ...) []domain.Question
- func SelectRandomQuestions(questions []domain.Question, n int, rng *rand.Rand) []domain.Question
- type CurrentUserProvider
- type ExportService
- type ImportResult
- type ImportService
- type ModeParams
- type SelectionMode
- type SessionConfig
- type SessionEngine
- func (e *SessionEngine) ActiveMode() SelectionMode
- func (e *SessionEngine) ActiveModeParams() ModeParams
- func (e *SessionEngine) EndSession() error
- func (e *SessionEngine) EndSessionByID(sessionID int64) error
- func (e *SessionEngine) GetNextSessionQuestion() *SessionQuestion
- func (e *SessionEngine) GetNextSessionQuestionForSession(sessionID int64) (*SessionQuestion, error)
- func (e *SessionEngine) LoadSession(sessionID int64) error
- func (e *SessionEngine) ModeNote() string
- func (e *SessionEngine) QueueLength() int
- func (e *SessionEngine) QueueLengthForSession(sessionID int64) (int, error)
- func (e *SessionEngine) RecordAttempt(questionID int64, selectedChoiceIDs []string, skipped bool, latencyMs int) (bool, error)
- func (e *SessionEngine) RecordAttemptForSession(sessionID int64, questionID int64, selectedChoiceIDs []string, skipped bool, ...) (bool, error)
- func (e *SessionEngine) StartSession(topicSlug string, n int, mode string) (int64, error)
- func (e *SessionEngine) StartSessionWithConfig(cfg SessionConfig) (int64, error)
- func (e *SessionEngine) WithStatsRepo(sr ports.StatsRepository) *SessionEngine
- type SessionQuestion
- type UserContext
- type WeakestResult
- type WeakestSubMode
Constants ¶
const DefaultMinAttempts = 3
DefaultMinAttempts is the minimum number of attempts a question must have before it is considered for weakest-question selection.
const DefaultMinTagAttempts = 5
DefaultMinTagAttempts is the minimum number of attempts required for a tag to be included in tag-based stats and weakest-tag selection.
Variables ¶
var ValidModes = map[SelectionMode]bool{ ModeBalanced: true, ModeRandom: true, ModeByDifficulty: true, ModeWeakest: true, }
ValidModes lists all accepted selection modes.
Functions ¶
func EnsureDefaultUser ¶
func EnsureDefaultUser(userRepo ports.UserRepository) ([]domain.User, error)
EnsureDefaultUser lists existing users and seeds a "local" profile if none exist. Returns the full user list (always ≥ 1 element).
func ModeDisplayName ¶
func ModeDisplayName(m SelectionMode) string
ModeDisplayName returns the user-facing label for a mode.
func ModeLabel ¶
func ModeLabel(mode SelectionMode, params ModeParams) string
ModeLabel builds a one-line context string for display in the quiz header.
func ModeParamsJSON ¶
func ModeParamsJSON(p ModeParams) string
ModeParamsJSON encodes mode params to a JSON string.
func ResolveUser ¶
ResolveUser finds the current user from the repository. It first tries the configuredUserID, then falls back to the "local" handle, then to the first available user. Returns the resolved user, whether it matched the configured ID, and any error.
func SelectByDifficulty ¶
func SelectByDifficulty( questions []domain.Question, stats map[int64]ports.QuestionStats, n int, difficulty string, rng *rand.Rand, ) (selected []domain.Question, note string)
SelectByDifficulty picks up to n questions matching the given difficulty, then applies Balanced selection within that filtered set. Returns the selected questions and a note string (non-empty if N was reduced).
func SelectQuestions ¶
func SelectQuestions( questions []domain.Question, stats map[int64]ports.QuestionStats, n int, rng *rand.Rand, ) []domain.Question
SelectQuestions picks up to n questions for a session, prioritizing unseen and weak questions. rng controls shuffle order (use a seeded *rand.Rand for deterministic tests).
Types ¶
type CurrentUserProvider ¶
CurrentUserProvider provides read/write access to the active local user profile.
type ExportService ¶
type ExportService struct {
// contains filtered or unexported fields
}
ExportService handles the export use case.
func NewExportService ¶
func NewExportService(topics ports.TopicRepository, questions ports.QuestionRepository) *ExportService
NewExportService creates an export service with the given dependencies.
func (*ExportService) Export ¶
func (s *ExportService) Export(topicSlug, outPath, format string) error
Export writes the canonical pack file for the given topic slug. format must be "yaml" or "json". outPath is the destination file.
func (*ExportService) ExportToBytes ¶
func (s *ExportService) ExportToBytes(topicSlug, format string) ([]byte, error)
ExportToBytes returns the serialized pack as bytes (for testing).
type ImportResult ¶
type ImportResult struct {
FilesProcessed int
Inserted int
Duplicates int
Invalid int
Errors []string
}
ImportResult summarizes the outcome of an import operation.
type ImportService ¶
type ImportService struct {
// contains filtered or unexported fields
}
ImportService handles the import use case.
func NewImportService ¶
func NewImportService(reader ports.PackReader, topics ports.TopicRepository, questions ports.QuestionRepository) *ImportService
NewImportService creates an import service with the given dependencies.
func (*ImportService) Import ¶
func (s *ImportService) Import(path string) (*ImportResult, error)
Import loads packs from a file or directory, validates, normalises, and persists.
func (*ImportService) ImportFromFS ¶
func (s *ImportService) ImportFromFS(fsys fs.FS) (*ImportResult, error)
ImportFromFS imports all YAML pack files from an fs.FS (e.g. an embedded file system). This is the same pipeline as [Import] but reads from the provided FS instead of the host filesystem.
type ModeParams ¶
type ModeParams struct {
Difficulty string `json:"difficulty,omitempty"`
WeakestSub WeakestSubMode `json:"weakest_sub,omitempty"`
WeakestTag string `json:"weakest_tag,omitempty"` // resolved tag name
}
ModeParams is the serialisable form of mode-specific parameters, persisted as JSON in the sessions table.
type SelectionMode ¶
type SelectionMode string
SelectionMode enumerates supported question selection strategies.
const ( // ModeBalanced selects questions using unseen → weak → random fill. ModeBalanced SelectionMode = "balanced" // ModeRandom selects questions in random order, ignoring prior attempts. ModeRandom SelectionMode = "random" // ModeByDifficulty filters to a chosen difficulty, then applies Balanced. ModeByDifficulty SelectionMode = "by_difficulty" // ModeWeakest focuses on the user's weakest tag or questions. ModeWeakest SelectionMode = "weakest" )
type SessionConfig ¶
type SessionConfig struct {
TopicSlug string
N int
Mode SelectionMode
// ByDifficulty params — only used when Mode == ModeByDifficulty.
Difficulty string // "easy", "medium", or "hard"
// Weakest params — only used when Mode == ModeWeakest.
WeakestSub WeakestSubMode
}
SessionConfig holds all parameters needed to start a session.
type SessionEngine ¶
type SessionEngine struct {
// contains filtered or unexported fields
}
SessionEngine manages the lifecycle of a practice session.
func NewSessionEngine ¶
func NewSessionEngine( topics ports.TopicRepository, questions ports.QuestionRepository, sessions ports.SessionRepository, attempts ports.AttemptRepository, userCtx CurrentUserProvider, rng *rand.Rand, ) *SessionEngine
NewSessionEngine creates a session engine with the given dependencies. If rng is nil, a time-seeded source is used.
func (*SessionEngine) ActiveMode ¶
func (e *SessionEngine) ActiveMode() SelectionMode
ActiveMode returns the resolved selection mode for the current session.
func (*SessionEngine) ActiveModeParams ¶
func (e *SessionEngine) ActiveModeParams() ModeParams
ActiveModeParams returns the resolved mode parameters for the current session.
func (*SessionEngine) EndSession ¶
func (e *SessionEngine) EndSession() error
EndSession marks the session as finished.
func (*SessionEngine) EndSessionByID ¶
func (e *SessionEngine) EndSessionByID(sessionID int64) error
EndSessionByID marks a specific session as finished and unloads in-memory state.
func (*SessionEngine) GetNextSessionQuestion ¶
func (e *SessionEngine) GetNextSessionQuestion() *SessionQuestion
GetNextSessionQuestion returns the next session question in the queue, or nil when all questions have been served.
func (*SessionEngine) GetNextSessionQuestionForSession ¶
func (e *SessionEngine) GetNextSessionQuestionForSession(sessionID int64) (*SessionQuestion, error)
GetNextSessionQuestionForSession returns next question for a specific session.
func (*SessionEngine) LoadSession ¶
func (e *SessionEngine) LoadSession(sessionID int64) error
LoadSession loads a persisted session into engine memory and sets it active. It enables resuming sessions across engine instances.
func (*SessionEngine) ModeNote ¶
func (e *SessionEngine) ModeNote() string
ModeNote returns any informational note from the selector (e.g., reduced N).
func (*SessionEngine) QueueLength ¶
func (e *SessionEngine) QueueLength() int
QueueLength returns the total number of questions selected for this session.
func (*SessionEngine) QueueLengthForSession ¶
func (e *SessionEngine) QueueLengthForSession(sessionID int64) (int, error)
QueueLengthForSession returns question count for a specific session.
func (*SessionEngine) RecordAttempt ¶
func (e *SessionEngine) RecordAttempt( questionID int64, selectedChoiceIDs []string, skipped bool, latencyMs int, ) (bool, error)
RecordAttempt evaluates correctness and persists the attempt.
func (*SessionEngine) RecordAttemptForSession ¶
func (e *SessionEngine) RecordAttemptForSession( sessionID int64, questionID int64, selectedChoiceIDs []string, skipped bool, latencyMs int, ) (bool, error)
RecordAttemptForSession evaluates correctness and persists an attempt for a specific session.
func (*SessionEngine) StartSession ¶
StartSession validates the topic, selects questions, persists a session row, and returns the session ID. mode should be "balanced" (or legacy "practice"). This method uses Balanced mode for backward compatibility.
func (*SessionEngine) StartSessionWithConfig ¶
func (e *SessionEngine) StartSessionWithConfig(cfg SessionConfig) (int64, error)
StartSessionWithConfig validates the topic, selects questions using the specified mode, persists a session row, and returns the session ID.
func (*SessionEngine) WithStatsRepo ¶
func (e *SessionEngine) WithStatsRepo(sr ports.StatsRepository) *SessionEngine
WithStatsRepo sets an optional stats repository for weakest-by-tag selection.
type SessionQuestion ¶
SessionQuestion holds session-scoped display state for one question. It preserves a pointer to the original question while allowing per-session shuffled presentation of choices.
type UserContext ¶
type UserContext struct {
// contains filtered or unexported fields
}
UserContext stores the current user ID in memory.
func NewUserContext ¶
func NewUserContext(initialUserID int64) *UserContext
NewUserContext creates a UserContext with an initial user ID.
func (*UserContext) CurrentUserID ¶
func (c *UserContext) CurrentUserID() int64
CurrentUserID returns the active user ID.
func (*UserContext) SetCurrentUserID ¶
func (c *UserContext) SetCurrentUserID(id int64)
SetCurrentUserID updates the active user ID.
type WeakestResult ¶
type WeakestResult struct {
Questions []domain.Question
Note string // informational note (e.g., fallback notice)
Tag string // the resolved weakest tag, if tag mode was used
}
WeakestResult holds the output of a weakest selection.
func SelectWeakestByQuestions ¶
func SelectWeakestByQuestions( questions []domain.Question, stats map[int64]ports.QuestionStats, n int, minAttempts int, rng *rand.Rand, ) WeakestResult
SelectWeakestByQuestions selects questions ranked by wrong rate. Candidates must have attempts >= minAttempts. If not enough weak questions exist, fills remainder with Balanced selection.
func SelectWeakestByTag ¶
func SelectWeakestByTag( questions []domain.Question, stats map[int64]ports.QuestionStats, tagStats []ports.TagStat, n int, rng *rand.Rand, ) WeakestResult
SelectWeakestByTag selects questions from the weakest tag. tagStats must already be filtered by minAttempts. If no valid tag is found, falls back to SelectWeakestByQuestions.
type WeakestSubMode ¶
type WeakestSubMode string
WeakestSubMode distinguishes between tag-based and question-based weakest.
const ( // WeakestByTag selects questions from the weakest tag. WeakestByTag WeakestSubMode = "by_tag" // WeakestByQuestion selects individual questions with the highest wrong rate. WeakestByQuestion WeakestSubMode = "by_question" )