Conversation
Outlines plan to merge calculators, replace scheduler inheritance with composition, rename public API, and restructure directories. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Unified StabilityCalculator + DifficultyCalculator into single MemoryStateCalculator with nextMemoryState() entry point - Moved calculateNextMemoryState logic from BaseScheduler into MemoryStateCalculator (pure calculation, not scheduling) - Moved IntervalCalculator and IntervalConstraintApplier to Algorithm/ - Deleted old separate calculator files All 125 tests pass — behavioral equivalence verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Created SchedulingPipeline struct with shared state and calculators - Created LearningStepsHandler for step progression logic - Created ShortTermScheduler (replaces BasicScheduler) composing Pipeline + LearningStepsHandler - Created LongTermScheduler (struct) composing Pipeline only - Updated SchedulerFactory to use new scheduler types - Deleted BaseScheduler class and old scheduler files Classes replaced with value-type structs. All 125 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- repeat(card:now:) → schedule(card:at:) - next(card:now:rating:) → schedule(card:at:rating:) - getRetrievability(card:now:) → formattedRetrievability(of:at:) - getRetrievabilityValue(card:now:) → retrievability(of:at:) - forget(card:now:) → forget(card:at:) - reschedule(currentCard:) → reschedule(card:) Updated all tests and examples. All 125 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move filtering, sorting, empty card creation, handler application, and result assembly from FSRS.reschedule() into RescheduleService.reschedule(). FSRS.reschedule() is now a 3-line delegate. Rename inner reschedule loop to replayReviews() for clarity. FSRS.swift: 243 → 189 lines.
- Delete ScheduledInterval value object (unused type) - Delete invalidScheduledInterval error case (unreachable) - Delete Factory.swift; replace fsrs() free function with direct FSRS<TestCard>() init in all tests - Move ConsoleLogger to test helpers - Relocate Core/ contents: FSRSAlgorithm → Algorithm/, FSRS.swift → root, FSRSErrors/Constants/ParameterManagement → Models/ - Remove empty Core/ directory Final structure: Algorithm/, Scheduling/, Models/, Protocols/, Services/, Strategies/, Utilities/, plus FSRS.swift at root.
21 new tests covering: - MemoryStateCalculator: init stability/difficulty ordering, forgetting curve properties, new vs existing card paths, short-term vs long-term, decay factor computation - SchedulingPipeline: elapsed days calculation, reps increment, memory state computation, retrievability, constrained interval ordering, schedule-with-interval, build log Total: 146 tests across 6 suites, all passing.
Rename the configuration struct and init parameter label across the entire codebase: - PartialFSRSParameters → FSRSConfiguration - FSRS(params:) → FSRS(configuration:) - FSRSAlgorithm(params:) → FSRSAlgorithm(configuration:) Kept as a top-level type rather than nested inside generic FSRS<Card> to avoid requiring FSRS<SomeCard>.Configuration in non-generic contexts.
PR Review: Full Architectural ReworkOverall this is a solid refactor. The shift from inheritance (BaseScheduler hierarchy) to composition (SchedulingPipeline) is the right direction for Swift, and the API renames eliminate the repeat keyword collision cleanly. Below are observations organized by severity. Issues1. Breaking public API without deprecation annotations The renamed methods and types (repeat/next/getRetrievability/getRetrievabilityValue/PartialFSRSParameters) have no @available(*, deprecated, renamed:...) annotations. Any existing consumers of the library will get compile errors with no migration guidance. Swift's renaming mechanism is designed exactly for this: // ParameterManagement.swift
@available(*, deprecated, renamed: "FSRSConfiguration")
public typealias PartialFSRSParameters = FSRSConfiguration
// FSRS.swift
@available(*, deprecated, renamed: "schedule(card:at:)")
public func repeat(card: Card, now: Date) throws -> RecordLog<Card> {
try schedule(card: card, at: now)
}This makes the transition non-breaking and compiler-guided. Even for a minor version bump, signalling intent is good practice. 2. Oversized tuple in computeConstrainedReviewIntervals The 6-element tuple return (suppressed with // swiftlint:disable:this large_tuple) is a code smell — it's a struct waiting to be named: // Instead of:
func computeConstrainedReviewIntervals(...) throws -> (hard: Int, good: Int, easy: Int, hardState: MemoryState, goodState: MemoryState, easyState: MemoryState)
// Consider:
struct ConstrainedReviewIntervals {
let hard: Int; let good: Int; let easy: Int
let hardState: MemoryState; let goodState: MemoryState; let easyState: MemoryState
}3. SchedulingPipeline exposes too much as public SchedulingPipeline.logger, memoryStateCalculator, intervalCalculator, elapsedDaysValue, and currentCard are all public. Since ShortTermScheduler and LongTermScheduler live in the same module, internal access is sufficient for all of these. Marking them public unnecessarily grows the library's API surface and locks in implementation details. 4. Redundant assignment in RescheduleService.reschedule var emptyCard = card
emptyCard.due = card.due // no-op, already copied from card
emptyCard.stability = 0The emptyCard.due = card.due line can be removed. 5. Schedulers access pipeline internals directly LongTermScheduler reaches into pipeline.logger, pipeline.currentCard, pipeline.reviewTime, pipeline.elapsedDaysValue, pipeline.algorithm, pipeline.intervalCalculator throughout its implementation. If SchedulingPipeline is meant to be an encapsulated unit of shared state, the schedulers should consume it through its public methods (computeMemoryState, computeRetrievability, etc.) rather than as a property bag. The current design gives composition the shape of inheritance — just accessing a bag of state with a different name. Minor / Nits6. formattedRetrievability naming ambiguity The distinction between formattedRetrievability(of:at:) -> String and retrievability(of:at:) -> Double may not be obvious at call sites. Consider retrievabilityString(of:at:) or at minimum a doc comment on formattedRetrievability calling out that it returns a percentage-formatted string like "85.00%". 7. swiftlint:disable:next function_body_length in ShortTermScheduler Two functions carry this suppression. The reviewState function in particular has an if rating == .again branch that could be a separate relearningState helper, both reducing the body length and making the branching intent explicit. 8. README "Verify examples compile" is still unchecked The PR description's own checklist item — [ ] Verify examples compile against new API names — is unresolved before merge. 9. SchedulingPipeline.buildLog captures pre-update state The pipeline increments currentCard.reps at init, but stability/difficulty are not yet updated when buildLog is called (schedulers update a local nextCard copy). This means the log entry records the pre-review stability/difficulty values. That appears intentional (log what the card was at review time), but deserves a doc comment explaining the deliberate choice, since it's easy to misread as a bug. What's Good
|
- Rename short variable names (r, r1 → retrievability, ret1) - Replace force unwraps with nil-coalescing in date arithmetic - Fix multiline argument formatting (one arg per line) - Remove superfluous swiftlint disable commands in ShortTermScheduler
PR Review: Full Architectural ReworkOverall this is a well-motivated refactoring with clear architectural goals. The move from class inheritance to struct composition is idiomatic Swift, and the API renaming resolves real issues (backtick-quoted Strengths
Issues1. Breaking changes with no migration path (major)This PR makes multiple breaking changes with no deprecation shims:
Any existing consumer of this library gets compilation failures with no guidance. For a library, the standard approach is to add 2. Large tuple return from
|
Summary
StabilityCalculator+DifficultyCalculatorinto unifiedMemoryStateCalculator, moved toAlgorithm/BaseScheduler/BasicSchedulerclass hierarchy, replaced withSchedulingPipelinestruct +ShortTermScheduler/LongTermSchedulerstructs composed via pipelinerepeat→schedule(card:at:),next→schedule(card:at:rating:),getRetrievability→retrievability(of:at:),PartialFSRSParameters→FSRSConfigurationRescheduleService,FSRS.swiftreduced from 243 to 189 linesScheduledInterval,Factory.swift; eliminatedCore/directory; organized intoAlgorithm/,Scheduling/,Models/, etc.MemoryStateCalculatorandSchedulingPipeline(146 total, all passing)Test plan
swift test)swift build)