Skip to content

fix(scheduler): datetime fidelity so FSRS learning steps stop collapsing to 'due today' - #58

Merged
ImmortalDemonGod merged 18 commits into
mainfrom
fix/fsrs-datetime-fidelity
Jun 29, 2026
Merged

fix(scheduler): datetime fidelity so FSRS learning steps stop collapsing to 'due today'#58
ImmortalDemonGod merged 18 commits into
mainfrom
fix/fsrs-datetime-fidelity

Conversation

@ImmortalDemonGod

@ImmortalDemonGod ImmortalDemonGod commented Jun 29, 2026

Copy link
Copy Markdown
Owner

AIV Verification Packet (v2.2)

Identification

Field Value
Repository github.com/ImmortalDemonGod/aiv-protocol
Change ID fsrs-datetime-fidelity
Commits dfd5d5f, 0440ed3, 47f0ba7, 5d96cc4, 5f034db, 103312d, 85959f3, e8df5fb, 966032e, fd41993, 2d28a38, d7a5cdf, 77929c7, f97a3ca, 661a3f9, b826924
Head SHA b826924
Base SHA fb1ae5a
Created 2026-06-29T20:47:54Z

Classification

classification:
  risk_tier: R1
  sod_mode: S0
  critical_surfaces: []
  blast_radius: component
  classification_rationale: "R1 correctness-critical FSRS scheduling path plus a DB schema migration (next_due_date/next_due DATE->TIMESTAMPTZ, +cards.step); blast radius is the scheduler/hub data flow and the kernel due predicates."
  classified_by: "Miguel Ingram"
  classified_at: "2026-06-29T20:47:54Z"

Claims

  1. Card.next_due_date and last_review_date are typed datetime (not date) so sub-day FSRS spacing is representable
  2. Card.step (Optional int) is added under extra=forbid for persisting the FSRS learning-step index
  3. No existing tests were modified or deleted during this change.
  4. FSRS_Scheduler.compute_next_state returns next_due as a datetime preserving sub-day learning-step spacing
  5. FSRS_Scheduler.compute_next_state restores and emits the FSRS learning step so Learning cards progress across reviews
  6. ReviewProcessor.process_review sets last_review_date from the full prior-review timestamp not its date
  7. ReviewProcessor.process_review persists the scheduler step via add_review_and_update_card new_step
  8. cards.next_due_date and reviews.next_due are TIMESTAMP WITH TIME ZONE and cards has a step column
  9. FlashcardDatabase.get_due_cards compares next_due_date against a timestamp, coercing a bare date to end-of-day
  10. FlashcardDatabase persists the FSRS step via the cards upsert and the post-review card update
  11. card_to_db_params_list includes card.step as the trailing value of the cards insert tuple
  12. start_review_flow computes days-until-due from next_due_date.date() so it works with datetime dues
  13. _review_all_logic computes days-until-due from next_due_date.date() so it works with datetime dues
  14. migrate_to_datetime_fidelity converts the due columns to TIMESTAMPTZ, adds cards.step, and backfills step=0 for Learning/Relearning idempotently
  15. test_scheduler asserts learning-step next_due is a future sub-day datetime and that step persistence graduates a card
  16. test_db asserts next_due_date.date() since the column is now a timestamp
  17. test_review_manager SchedulerOutput fixtures include step and use datetime next_due
  18. test_review_processor SchedulerOutput fixtures include step and assert datetime next_due
  19. test_review_logic_duplication SchedulerOutput fixture includes the step field
  20. test_review_ui mocks next_due_date as a datetime so .date() resolves
  21. test_review_all_logic mocks next_due_date as a datetime so .date() resolves

Evidence References

# Evidence File Commit SHA Classes
1 EVIDENCE_FLASHCORE_MODELS.md dfd5d5f A, B, E
2 EVIDENCE_FLASHCORE_SCHEDULER.md 0440ed3 A, B, E
3 EVIDENCE_FLASHCORE_REVIEW_PROCESSOR.md 47f0ba7 A, B, E
4 EVIDENCE_FLASHCORE_DB_SCHEMA.md 5d96cc4 A, B, E
5 EVIDENCE_FLASHCORE_DB_DATABASE.md 5f034db A, B, E
6 EVIDENCE_FLASHCORE_DB_DB_UTILS.md 103312d A, B, E
7 EVIDENCE_FLASHCORE_CLI_REVIEW_UI.md 85959f3 A, B, E
8 EVIDENCE_FLASHCORE_CLI__REVIEW_ALL_LOGIC.md e8df5fb A, B, E
9 EVIDENCE_SCRIPTS_MIGRATE_TO_DATETIME_FIDELITY.md 966032e A, B, E
10 EVIDENCE_TESTS_TEST_SCHEDULER.md fd41993 A, B, E
11 EVIDENCE_TESTS_TEST_DB.md 2d28a38 A, B, E
12 EVIDENCE_TESTS_TEST_REVIEW_MANAGER.md d7a5cdf A, B, E
13 EVIDENCE_TESTS_TEST_REVIEW_PROCESSOR.md 77929c7 A, B, E
14 EVIDENCE_TESTS_TEST_REVIEW_LOGIC_DUPLICATION.md f97a3ca A, B, E
15 EVIDENCE_TESTS_CLI_TEST_REVIEW_UI.md 661a3f9 A, B, E
16 EVIDENCE_TESTS_CLI_TEST_REVIEW_ALL_LOGIC.md b826924 A, B, E

Class E (Intent Alignment)

  • Requirement: Live deck 2026-06-29: FSRS learning steps collapsed to a 0-day due-today because next_due was truncated to a DATE and the learning step index was never persisted; Learning cards never spaced or graduated except via Easy. Fix restores datetime fidelity + step persistence.

Class B (Referential Evidence)

Scope Inventory (from 69 file references across evidence files)

  • flashcore/models.py#L11
  • flashcore/models.py#L61
  • flashcore/models.py#L64-L66
  • flashcore/models.py#L69
  • flashcore/models.py#L71-L75
  • flashcore/models.py#L88-L95
  • flashcore/models.py#L252
  • flashcore/models.py#L254-L258
  • flashcore/scheduler.py#L41-L43
  • flashcore/scheduler.py#L48
  • flashcore/scheduler.py#L208-L212
  • flashcore/scheduler.py#L214-L216
  • flashcore/scheduler.py#L218
  • flashcore/scheduler.py#L268-L270
  • flashcore/scheduler.py#L277
  • flashcore/review_processor.py#L104-L106
  • flashcore/review_processor.py#L132-L133
  • flashcore/review_processor.py#L135-L137
  • flashcore/review_processor.py#L140
  • flashcore/db/schema.py#L17
  • flashcore/db/schema.py#L21
  • flashcore/db/schema.py#L46
  • flashcore/db/database.py#L145-L146
  • flashcore/db/database.py#L176-L180
  • flashcore/db/database.py#L400-L407
  • flashcore/db/database.py#L416
  • flashcore/db/database.py#L453-L462
  • flashcore/db/database.py#L467
  • flashcore/db/database.py#L533
  • flashcore/db/database.py#L704
  • flashcore/db/database.py#L717-L718
  • flashcore/db/database.py#L726
  • flashcore/db/database.py#L733-L736
  • flashcore/db/database.py#L746
  • flashcore/db/database.py#L758
  • flashcore/db/database.py#L783-L786
  • flashcore/db/database.py#L807
  • flashcore/db/db_utils.py#L97
  • flashcore/cli/review_ui.py#L125-L127
  • flashcore/cli/_review_all_logic.py#L88
  • scripts/migrate_to_datetime_fidelity.py#L1-L151
  • tests/test_scheduler.py#L53-L61
  • tests/test_scheduler.py#L128-L131
  • tests/test_scheduler.py#L215
  • tests/test_scheduler.py#L241-L242
  • tests/test_scheduler.py#L254
  • tests/test_scheduler.py#L369-L370
  • tests/test_scheduler.py#L395-L396
  • tests/test_scheduler.py#L505
  • tests/test_scheduler.py#L510
  • tests/test_scheduler.py#L527
  • tests/test_scheduler.py#L530
  • tests/test_scheduler.py#L613-L614
  • tests/test_scheduler.py#L820-L901
  • tests/test_db.py#L1009-L1010
  • tests/test_db.py#L1050-L1051
  • tests/test_review_manager.py#L48
  • tests/test_review_manager.py#L52
  • tests/test_review_manager.py#L563
  • tests/test_review_processor.py#L57
  • tests/test_review_processor.py#L62
  • tests/test_review_logic_duplication.py#L68
  • tests/cli/test_review_ui.py#L5
  • tests/cli/test_review_ui.py#L55
  • tests/cli/test_review_ui.py#L98-L100
  • tests/cli/test_review_ui.py#L341
  • tests/cli/test_review_ui.py#L382
  • tests/cli/test_review_all_logic.py#L138-L140
  • tests/cli/test_review_all_logic.py#L538-L540

Class F (Provenance)

The defect was real, observed in production before the fix.

  • Production symptom (2026-06-29): on the live deck, 21 Learning-state cards all had next_due_date pinned to the current calendar date (00:00 = already overdue) and were re-drilled repeatedly within a single session; only Easy-rated cards ever advanced to a forward date. Query evidence: reviews for 2026-06-29 showed every Again/Hard/Good rating producing scheduled_days_interval = 0 and next_due = today, while Easy produced multi-day intervals.

  • Root cause, two parts (both fixed here):

    1. FSRS_Scheduler.compute_next_state returned next_due = updated_fsrs_card.due.date() — truncating py-fsrs's datetime due (1m/10m learning steps) to a bare date, collapsing every sub-day step to "due today".
    2. A fresh FSRSCard() was rebuilt each review without restoring card.step, and Card had no step field, so a Learning card restarted at step 0 every review and could only leave Learning via Easy.
  • RED -> GREEN regressions (fail on the pre-fix engine, pass after — tests/test_scheduler.py):

    • test_learning_step_due_is_future_subday — asserts a learning step's next_due is a future, same-day, sub-day datetime (RED: pre-fix returned review_ts.date()).
    • test_step_persistence_lets_learning_card_graduate — asserts repeated Good graduates the card once step is persisted (RED: pre-fix looped on "due today" forever).
  • End-to-end on a copy of the real DB (post-migration): a Learning card moved from 2026-06-28 00:00 (overdue) -> now + 0:10:00 (real 10-minute step, step 0->1) -> graduated to a 6-day interval on the next Good.

  • Upstream confirmation: py-fsrs 6.3.x — Card.due is a datetime; learning_steps = (1m, 10m). The minute-level scheduling is correct upstream; the truncation was flashcore-side.


Verification Methodology

Zero-Touch Mandate: Verifier inspects artifacts only.
Evidence was collected by aiv commit during the change lifecycle.
Packet generated by aiv close.


Known Limitations

  • Evidence references point to Layer 1 evidence files at specific commit SHAs.
    Use git show <sha>:.github/aiv-evidence/<file> to retrieve.

Summary

Change 'fsrs-datetime-fidelity': 16 commit(s) across 16 file(s).

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 219f068e-365c-421c-bd5c-7ee1d7e1d348

📥 Commits

Reviewing files that changed from the base of the PR and between f857cbe and f49ed7e.

📒 Files selected for processing (33)
  • .github/aiv-evidence/EVIDENCE_FLASHCORE_CLI_REVIEW_UI.md
  • .github/aiv-evidence/EVIDENCE_FLASHCORE_CLI__REVIEW_ALL_LOGIC.md
  • .github/aiv-evidence/EVIDENCE_FLASHCORE_DB_DATABASE.md
  • .github/aiv-evidence/EVIDENCE_FLASHCORE_DB_DB_UTILS.md
  • .github/aiv-evidence/EVIDENCE_FLASHCORE_DB_SCHEMA.md
  • .github/aiv-evidence/EVIDENCE_FLASHCORE_MODELS.md
  • .github/aiv-evidence/EVIDENCE_FLASHCORE_REVIEW_PROCESSOR.md
  • .github/aiv-evidence/EVIDENCE_FLASHCORE_SCHEDULER.md
  • .github/aiv-evidence/EVIDENCE_SCRIPTS_MIGRATE_TO_DATETIME_FIDELITY.md
  • .github/aiv-evidence/EVIDENCE_TESTS_CLI_TEST_REVIEW_ALL_LOGIC.md
  • .github/aiv-evidence/EVIDENCE_TESTS_CLI_TEST_REVIEW_UI.md
  • .github/aiv-evidence/EVIDENCE_TESTS_TEST_DB.md
  • .github/aiv-evidence/EVIDENCE_TESTS_TEST_REVIEW_LOGIC_DUPLICATION.md
  • .github/aiv-evidence/EVIDENCE_TESTS_TEST_REVIEW_MANAGER.md
  • .github/aiv-evidence/EVIDENCE_TESTS_TEST_REVIEW_PROCESSOR.md
  • .github/aiv-evidence/EVIDENCE_TESTS_TEST_SCHEDULER.md
  • .github/aiv-packets/PACKET_fsrs_datetime_fidelity.md
  • flashcore/cli/_review_all_logic.py
  • flashcore/cli/review_ui.py
  • flashcore/db/database.py
  • flashcore/db/db_utils.py
  • flashcore/db/schema.py
  • flashcore/models.py
  • flashcore/review_processor.py
  • flashcore/scheduler.py
  • scripts/migrate_to_datetime_fidelity.py
  • tests/cli/test_review_all_logic.py
  • tests/cli/test_review_ui.py
  • tests/test_db.py
  • tests/test_review_logic_duplication.py
  • tests/test_review_manager.py
  • tests/test_review_processor.py
  • tests/test_scheduler.py

Walkthrough

This PR upgrades FSRS scheduling to preserve full UTC datetime precision (replacing bare date) for due dates and last-review timestamps, adds a step field to Card and SchedulerOutput to persist the FSRS learning/relearning step across reviews, propagates these changes through the scheduler, review processor, and database layers, adds an idempotent migration script, and separately fixes a _summary_ placeholder in the flashcore/models.py module docstring with supporting tests and AIV evidence artifacts.

Changes

FSRS Datetime Fidelity and Step Persistence

Layer / File(s) Summary
Model and schema field type changes
flashcore/models.py, flashcore/db/schema.py
Card.last_review_date, Card.next_due_date, and Review.next_due change from date to Optional[datetime]; Card.step: Optional[int] is added. DB schema columns cards.next_due_date and reviews.next_due change to TIMESTAMP WITH TIME ZONE.
Scheduler datetime precision and step output
flashcore/scheduler.py
SchedulerOutput.next_due changes from date to datetime; step field added. compute_next_state restores card.step into fsrs_card, passes full UTC datetimes via _ensure_utc, and populates SchedulerOutput.step from updated_fsrs_card.step without truncation.
Database step persistence and datetime cutoffs
flashcore/db/database.py, flashcore/db/db_utils.py
UPSERT SQL adds step with NULL-preserving conflict logic; get_due_card_count and get_due_cards coerce date inputs to UTC end-of-day datetime; get_database_stats predicate changes to < CURRENT_DATE + INTERVAL 1 DAY; _update_card_after_review, _execute_review_transaction, and add_review_and_update_card gain new_step parameter; card_to_db_params_list appends card.step.
ReviewProcessor full-datetime wiring
flashcore/review_processor.py
process_review assigns card.last_review_date from full prior_review.ts datetime instead of .date(); passes scheduler_output.step as new_step to add_review_and_update_card; caches updated_card.last_review_date as the full timestamp.
Migration script for existing databases
scripts/migrate_to_datetime_fidelity.py
New idempotent CLI conditionally alters cards.next_due_date and reviews.next_due to TIMESTAMP WITH TIME ZONE, adds cards.step integer column if absent, and backfills step=0 for Learning/Relearning rows with NULL step.
Tests updated for datetime fidelity and step
tests/test_scheduler.py, tests/test_review_processor.py, tests/test_review_manager.py, tests/test_review_logic_duplication.py, tests/test_db.py
Scheduler tests now assert next_due is a datetime > review_ts and use replace() for chained review timestamps; two new regression tests verify learning-step subday fidelity and step-persistence graduation. Mock fixtures in processor/manager/duplication tests updated to use timezone-aware datetime for next_due and include step. DB tests compare next_due_date.date() for upsert preservation assertions.

F354 Module Docstring Fix and AIV Evidence

Layer / File(s) Summary
Docstring fix, tests, and bug catalog
flashcore/models.py, tests/test_models.py, tests/test_models.py.bug-catalog.md
flashcore/models.py module docstring replaced from _summary_ placeholder to a description of five exported types. Three tests added to tests/test_models.py verifying the docstring is not a placeholder, references at least one exported type name, and that referenced names resolve to classes. Bug catalog enumerates B1/B2/B3 defect variants mapped to test cases.
Black formatting fix in review_ui.py
flashcore/cli/review_ui.py
days_until_due_date computation in start_review_flow split into a multi-line parenthesized expression to satisfy Black's 79-character limit and unblock CI lint.
AIV verification packets and evidence artifacts
.github/aiv-packets/*, .github/aiv-evidence/*, .gitignore
New and updated AIV evidence markdown files and verification packets for the CI formatting change, docstring implementation change, design-tests stage, and full PR verification. Includes evidence manifest, baseline-red/head-green pytest snapshots, negative-search and docstring-diff artifacts. .gitignore updated to exclude AIV scaffolding directories.

Sequence Diagram(s)

sequenceDiagram
    participant ReviewProcessor
    participant FSRS_Scheduler
    participant FlashcoreDB

    ReviewProcessor->>FSRS_Scheduler: compute_next_state(card with step, last_review_date as datetime)
    FSRS_Scheduler-->>ReviewProcessor: SchedulerOutput(next_due: datetime, step: int)
    ReviewProcessor->>FlashcoreDB: add_review_and_update_card(review, new_card_state, new_step=step)
    FlashcoreDB-->>ReviewProcessor: updated Card(next_due_date: datetime, step: int)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ImmortalDemonGod/flashcore#9: Introduced the Pydantic Card/Review/Session model fields and tests/test_models.py; this PR updates those same models' time/date field types and adds module-docstring coverage.
  • ImmortalDemonGod/flashcore#15: Established the card-based compute_next_state interface in flashcore/scheduler.py and flashcore/review_processor.py; this PR extends that interface with step and full UTC datetime precision.
  • ImmortalDemonGod/flashcore#32: Continued F169/C2 fix work in flashcore/review_processor.py and flashcore/scheduler.py using card.last_review_date; this PR further upgrades that field to full UTC datetime precision.

Poem

🐇 Hoppity-hop through the calendar gate,
No more bare dates that arrive far too late!
Sub-day precision now flows through each step,
The learning card graduates — no more misstep.
AIV packets all sealed with a bow,
The docstring's no placeholder — onward we go! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main scheduler fix: preserving datetime fidelity so FSRS learning steps no longer collapse to due today.
Docstring Coverage ✅ Passed Docstring coverage is 88.57% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/fsrs-datetime-fidelity

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
flashcore/db/db_utils.py (1)

50-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the tuple-order docstring to include step.

card_to_db_params_list() now emits a trailing card.step, but the docstring still documents tag_count as the last field. That leaves the bulk-insert positional contract inaccurate.

Proposed fix
-        front_length, back_length, has_media, tag_count).
+        front_length, back_length, has_media, tag_count, step).

Also applies to: 97-97

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashcore/db/db_utils.py` around lines 50 - 63, The positional tuple contract
in card_to_db_params_list is outdated because the function now includes
card.step but the docstring still ends with tag_count. Update the docstring in
card_to_db_params_list to document the full tuple order with step as the final
field, and make sure any related bulk-insert tuple documentation stays
consistent with the emitted order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@flashcore/cli/review_ui.py`:
- Around line 125-127: The due-date calculation in review_ui’s card-update flow
mixes a datetime value with date.today(), which can raise a TypeError. Update
the logic around days_until_due_date to use matching types by converting
updated_card.next_due_date to a date or by comparing both values as UTC
datetimes before computing the difference. Keep the fix localized to the code
that builds the due-date message so the arithmetic and formatting stay
consistent.

In `@flashcore/db/database.py`:
- Line 533: `get_database_stats()` is still using a timezone-sensitive
`CURRENT_DATE + INTERVAL 1 DAY` cutoff for `due_count`, which can diverge from
the UTC normalization used by `get_due_card_count()` and `get_due_cards()`.
Update the `due_count` expression in `get_database_stats()` to use the same UTC
end-of-day boundary logic as the due queue helpers, so deck stats and due cards
agree around UTC midnight.

In `@flashcore/models.py`:
- Around line 93-100: Constrain Card.step to non-negative values so invalid
learning-step indices fail fast instead of flowing into fsrs_card.step. Update
the Card model in flashcore/models.py by adding a non-negative validation rule
to the step field (for example via Field constraints or a validator) while
preserving the existing Optional[int] behavior for None. Keep the check close to
the Card definition so any caller, including flashcore/scheduler.py, is
protected from negative values.

In `@flashcore/scheduler.py`:
- Around line 206-216: Normalize review_ts to UTC before computing elapsed_days
in the scheduler flow so date-based deltas are consistent. In the code path that
sets fsrs_card.last_review and later derives elapsed_days from review_ts.date(),
make sure the same _ensure_utc conversion is applied to review_ts first, then
use the normalized timestamp for the day difference calculation. Keep this
change near the existing card.last_review_date handling in scheduler logic so
FSRS receives the correct elapsed-day count.

In `@scripts/migrate_to_datetime_fidelity.py`:
- Around line 58-76: The migration checks in migrate_to_datetime_fidelity.py are
skipping any column whose type merely contains “TIMESTAMP,” which incorrectly
treats plain TIMESTAMP as already migrated. Update the guards around the
cards.next_due_date and reviews.next_due alterations to skip only when _col_type
returns TIMESTAMP WITH TIME ZONE (or an equivalent exact timestamptz match), so
plain TIMESTAMP columns still get altered to TIMESTAMP WITH TIME ZONE.

In `@tests/test_scheduler.py`:
- Around line 128-131: The follow-up review setup in the scheduler tests is
overriding FSRS’s returned due time by using replace() to pin reviews to fixed
clock times, which can make the test run before the card is actually due. Update
the affected test cases to use the scheduled timestamp from the prior result
directly (for example, derive the next review from initial_good_result.next_due
without forcing a specific hour/minute) in the test helpers and scenarios that
reference review2_ts and similar follow-up timestamps.
- Around line 527-530: The test helper add_review_side_effect now captures
new_step, but the assertion never verifies it, so a regression in
ReviewProcessor forwarding scheduler_output.step to add_review_and_update_card()
would go unnoticed. Update the test around add_review_side_effect/captured to
assert that the persisted new_step matches the expected scheduler output step,
alongside the existing review and new_card_state checks.

---

Nitpick comments:
In `@flashcore/db/db_utils.py`:
- Around line 50-63: The positional tuple contract in card_to_db_params_list is
outdated because the function now includes card.step but the docstring still
ends with tag_count. Update the docstring in card_to_db_params_list to document
the full tuple order with step as the final field, and make sure any related
bulk-insert tuple documentation stays consistent with the emitted order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce1c2859-5391-49c6-ab1f-faaa4d640e02

📥 Commits

Reviewing files that changed from the base of the PR and between fb1ae5a and f857cbe.

📒 Files selected for processing (28)
  • .github/aiv-evidence/EVIDENCE_FLASHCORE_CLI_REVIEW_UI.md
  • .github/aiv-evidence/EVIDENCE_TESTS_TEST_MODELS.PY.BUG_CATALOG.MD.md
  • .github/aiv-evidence/EVIDENCE_TESTS_TEST_MODELS.md
  • .github/aiv-packets/PACKET_flashcore_f354_ci.md
  • .github/aiv-packets/PACKET_flashcore_f354_impl.md
  • .github/aiv-packets/PACKET_flashcore_f354_tests.md
  • .github/aiv-packets/VERIFICATION_PACKET_PR_FLASHCORE_F354.md
  • .github/aiv-packets/evidence/flashcore-f354/MANIFEST.md
  • .github/aiv-packets/evidence/flashcore-f354/baseline_red.txt
  • .github/aiv-packets/evidence/flashcore-f354/class_c_negative_search.txt
  • .github/aiv-packets/evidence/flashcore-f354/class_d_docstring_diff.txt
  • .github/aiv-packets/evidence/flashcore-f354/head_green.txt
  • .gitignore
  • flashcore/cli/review_ui.py
  • flashcore/db/database.py
  • flashcore/db/db_utils.py
  • flashcore/db/schema.py
  • flashcore/models.py
  • flashcore/review_processor.py
  • flashcore/scheduler.py
  • scripts/migrate_to_datetime_fidelity.py
  • tests/test_db.py
  • tests/test_models.py
  • tests/test_models.py.bug-catalog.md
  • tests/test_review_logic_duplication.py
  • tests/test_review_manager.py
  • tests/test_review_processor.py
  • tests/test_scheduler.py

Comment thread flashcore/cli/review_ui.py
Comment thread flashcore/db/database.py
deck_name,
COUNT(*) AS card_count,
COUNT(CASE WHEN next_due_date <= CURRENT_DATE OR next_due_date IS NULL THEN 1 END) AS due_count
COUNT(CASE WHEN next_due_date < CURRENT_DATE + INTERVAL 1 DAY OR next_due_date IS NULL THEN 1 END) AS due_count

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In DuckDB 1.0.0, when TIMESTAMP WITH TIME ZONE values are compared against CURRENT_DATE or CURRENT_DATE + INTERVAL 1 DAY, is CURRENT_DATE evaluated in the connection/session timezone rather than UTC?

💡 Result:

In DuckDB 1.0.0, the CURRENT_DATE function returns the current date in UTC, not the connection or session timezone [1][2]. Because DuckDB 1.0.0 uses UTC for CURRENT_DATE, comparing it directly against a TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ) value—which inherently represents an absolute point in time—will involve an implicit cast. When comparing a DATE to a TIMESTAMPTZ, DuckDB promotes the DATE to a TIMESTAMPTZ by treating it as midnight (00:00:00) in the configured connection time zone [3][4]. Consequently, even though CURRENT_DATE itself is UTC-based, the comparison logic often effectively introduces the connection's session timezone through this implicit promotion/casting process [3]. If your goal is to perform comparisons that strictly respect the session's local timezone for both the date component and the timestamp, you should be aware of this behavior, as it can lead to results that change depending on the current TIMEZONE setting [3]. Note that this behavior was identified as a PostgreSQL compatibility issue and has been updated in subsequent versions (post-1.0.0) to make CURRENT_DATE aware of the configured local timezone [5][6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and locate due-card helpers.
ast-grep outline flashcore/db/database.py --view expanded || true
printf '\n--- matches ---\n'
rg -n "get_due_card_count|get_due_cards|due_count|CURRENT_DATE|INTERVAL 1 DAY|end-of-day|UTC" flashcore/db/database.py flashcore -S || true

printf '\n--- relevant slice ---\n'
sed -n '430,590p' flashcore/db/database.py

Repository: ImmortalDemonGod/flashcore

Length of output: 12223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '370,430p' flashcore/db/database.py

Repository: ImmortalDemonGod/flashcore

Length of output: 2464


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "next_due_date|TIMESTAMPTZ|timezone|UTC timestamp|full UTC datetime" flashcore/models.py flashcore/db/database.py flashcore/scheduler.py -S

Repository: ImmortalDemonGod/flashcore

Length of output: 4325


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '141,180p' flashcore/db/database.py

Repository: ImmortalDemonGod/flashcore

Length of output: 2187


🌐 Web query:

DuckDB 1.0.0 comparison TIMESTAMPTZ against CURRENT_DATE + INTERVAL 1 DAY implicit cast timezone boundary UTC end of day

💡 Result:

In DuckDB (version 1.0.0 and subsequent releases), comparing a TIMESTAMPTZ column against CURRENT_DATE + INTERVAL 1 DAY involves specific implicit casting and timezone behaviors that can significantly impact results [1][2]. Key mechanisms to consider: 1. DATE to TIMESTAMPTZ Casting: When comparing a TIMESTAMPTZ value with a DATE value (or an expression resulting in a DATE, such as CURRENT_DATE + INTERVAL), DuckDB implicitly casts the DATE to TIMESTAMPTZ [1][2]. This cast promotes the DATE to midnight (00:00:00) in the user's current configured timezone [1][2]. 2. Timezone Sensitivity: Because the promotion to midnight is timezone-dependent, the result of the comparison tstz_column <= CURRENT_DATE + INTERVAL 1 DAY will vary based on the session's current timezone setting (e.g., 'America/Los_Angeles' vs 'UTC' vs 'HST') [1][2]. Changing the timezone effectively shifts the "boundary" of what represents the start of that next day, potentially including or excluding specific timestamp instants near the midnight threshold [1][2]. 3. CURRENT_DATE behavior: Since recent updates (addressing PostgreSQL compatibility), CURRENT_DATE returns the current date in the session's local timezone rather than UTC [3][4]. 4. Practical Implications: - Boundary sensitivity: Comparisons using these expressions often result in "off-by-one" day errors or unexpected filtering results because the implicit cast assumes the local midnight boundary [1][2]. - Best Practice: To avoid these implicit timezone dependencies and boundary issues, it is recommended to perform temporal calculations using explicit conversions or by keeping data in a consistent timezone (preferably UTC) until display, or by using plain TIMESTAMP (without time zone) types for internal storage when the offset context is not required [1][2]. If you must compare them, verify the result in your target timezone using CAST(expression AS TIMESTAMPTZ) to observe the specific instant being generated [1]. For exact control, avoid relying on the implicit cast. Instead, explicitly cast the date to your desired timezone before performing the comparison [5][2].

Citations:


Use the same UTC end-of-day cutoff in get_database_stats()

get_due_card_count() and get_due_cards() already normalize bare dates to 23:59:59.999999 UTC, but due_count still uses CURRENT_DATE + INTERVAL 1 DAY. That boundary is timezone-dependent in DuckDB and can make deck stats disagree with the due queue around UTC midnight.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashcore/db/database.py` at line 533, `get_database_stats()` is still using
a timezone-sensitive `CURRENT_DATE + INTERVAL 1 DAY` cutoff for `due_count`,
which can diverge from the UTC normalization used by `get_due_card_count()` and
`get_due_cards()`. Update the `due_count` expression in `get_database_stats()`
to use the same UTC end-of-day boundary logic as the due queue helpers, so deck
stats and due cards agree around UTC midnight.

Comment thread flashcore/models.py
Comment on lines +93 to +100
step: Optional[int] = Field(
default=None,
description=(
"FSRS learning/relearning step index (None in Review/New state)."
" Persisted so a card progresses through its learning steps"
" across reviews instead of restarting at step 0 each time."
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Constrain Card.step to non-negative values.

flashcore/scheduler.py forwards this field straight into fsrs_card.step, so a negative value from the DB or any caller would silently corrupt learning-step progression instead of failing fast.

Suggested fix
     step: Optional[int] = Field(
         default=None,
+        ge=0,
         description=(
             "FSRS learning/relearning step index (None in Review/New state)."
             " Persisted so a card progresses through its learning steps"
             " across reviews instead of restarting at step 0 each time."
         ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
step: Optional[int] = Field(
default=None,
description=(
"FSRS learning/relearning step index (None in Review/New state)."
" Persisted so a card progresses through its learning steps"
" across reviews instead of restarting at step 0 each time."
),
)
step: Optional[int] = Field(
default=None,
ge=0,
description=(
"FSRS learning/relearning step index (None in Review/New state)."
" Persisted so a card progresses through its learning steps"
" across reviews instead of restarting at step 0 each time."
),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashcore/models.py` around lines 93 - 100, Constrain Card.step to
non-negative values so invalid learning-step indices fail fast instead of
flowing into fsrs_card.step. Update the Card model in flashcore/models.py by
adding a non-negative validation rule to the step field (for example via Field
constraints or a validator) while preserving the existing Optional[int] behavior
for None. Keep the check close to the Card definition so any caller, including
flashcore/scheduler.py, is protected from negative values.

Comment thread flashcore/scheduler.py
Comment on lines +206 to +216
# Restore the learning/relearning step so the card progresses through
# its steps instead of restarting at step 0 every review. Without this,
# a Learning card never advances and can only escape via Easy.
if card.step is not None:
fsrs_card.step = card.step
if card.next_due_date:
fsrs_card.due = datetime.datetime.combine(
card.next_due_date,
datetime.time(0, 0, 0),
tzinfo=datetime.timezone.utc,
)
# next_due_date is now a full UTC datetime — feed it to FSRS as-is
# (no date->midnight rounding) so sub-day step dues are honored.
fsrs_card.due = self._ensure_utc(card.next_due_date)
if card.last_review_date is not None:
fsrs_card.last_review = datetime.datetime.combine(
card.last_review_date,
datetime.time(0, 0, 0),
tzinfo=datetime.timezone.utc,
)
fsrs_card.last_review = self._ensure_utc(card.last_review_date)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Normalize review_ts before deriving elapsed_days.

card.last_review_date is converted to UTC here, but the later day diff still uses review_ts.date() before normalization. A non-UTC review_ts can therefore cross a date boundary and feed FSRS the wrong elapsed-day count.

Suggested fix
-        # Manually calculate elapsed_days for the current review.
+        utc_review_ts = self._ensure_utc(review_ts)
+
+        # Manually calculate elapsed_days for the current review.
         if hasattr(fsrs_card, "last_review") and fsrs_card.last_review:
             elapsed_days = (
-                review_ts.date() - fsrs_card.last_review.date()
+                utc_review_ts.date() - fsrs_card.last_review.date()
             ).days
         else:
             # For a new card, there are no elapsed days since a prior review.
             elapsed_days = 0

         # Now, apply the new review to the final state.
         current_fsrs_rating = self._map_flashcore_rating_to_fsrs(new_rating)
-        utc_review_ts = self._ensure_utc(review_ts)
         try:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashcore/scheduler.py` around lines 206 - 216, Normalize review_ts to UTC
before computing elapsed_days in the scheduler flow so date-based deltas are
consistent. In the code path that sets fsrs_card.last_review and later derives
elapsed_days from review_ts.date(), make sure the same _ensure_utc conversion is
applied to review_ts first, then use the normalized timestamp for the day
difference calculation. Keep this change near the existing card.last_review_date
handling in scheduler logic so FSRS receives the correct elapsed-day count.

Comment on lines +58 to +76
if t and "TIMESTAMP" not in t.upper():
con.execute(
"ALTER TABLE cards ALTER COLUMN next_due_date "
"TYPE TIMESTAMP WITH TIME ZONE"
)
report["actions"].append(f"cards.next_due_date {t} -> TIMESTAMPTZ")
else:
report["actions"].append(f"cards.next_due_date already {t} (skip)")

# 2. reviews.next_due DATE -> TIMESTAMPTZ
t = _col_type(con, "reviews", "next_due")
if t and "TIMESTAMP" not in t.upper():
con.execute(
"ALTER TABLE reviews ALTER COLUMN next_due "
"TYPE TIMESTAMP WITH TIME ZONE"
)
report["actions"].append(f"reviews.next_due {t} -> TIMESTAMPTZ")
else:
report["actions"].append(f"reviews.next_due already {t} (skip)")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In DuckDB 1.0.0, what exact values can information_schema.columns.data_type return for TIMESTAMP and TIMESTAMP WITH TIME ZONE columns?

💡 Result:

In DuckDB 1.0.0, the information_schema.columns.data_type column returns the following values for timestamp-related columns: * For TIMESTAMP columns, it returns 'TIMESTAMP'. * For TIMESTAMP WITH TIME ZONE columns, it returns 'TIMESTAMP WITH TIME ZONE'. These values correspond to the primary type names used in DuckDB's SQL dialect [1][2]. While DuckDB provides various timestamp aliases (e.g., DATETIME for TIMESTAMP or TIMESTAMPTZ for TIMESTAMP WITH TIME ZONE), the information_schema typically reports the canonical type name defined in the system catalog [2][3]. Note that DuckDB also supports other specific timestamp variants (such as TIMESTAMP_MS, TIMESTAMP_NS, and TIMESTAMP_S) which may be returned as their respective internal type names if used [4][1].

Citations:


Only skip columns that are already TIMESTAMP WITH TIME ZONE.

The current guard treats any type containing "TIMESTAMP" as already migrated, so a plain TIMESTAMP column will be skipped and never upgraded to TIMESTAMP WITH TIME ZONE. That leaves the schema short of the intended target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/migrate_to_datetime_fidelity.py` around lines 58 - 76, The migration
checks in migrate_to_datetime_fidelity.py are skipping any column whose type
merely contains “TIMESTAMP,” which incorrectly treats plain TIMESTAMP as already
migrated. Update the guards around the cards.next_due_date and reviews.next_due
alterations to skip only when _col_type returns TIMESTAMP WITH TIME ZONE (or an
equivalent exact timestamptz match), so plain TIMESTAMP columns still get
altered to TIMESTAMP WITH TIME ZONE.

Comment thread tests/test_scheduler.py
Comment thread tests/test_scheduler.py
Comment on lines +527 to +530
def add_review_side_effect(*, review, new_card_state, new_step=None):
captured["review"] = review
captured["new_card_state"] = new_card_state
captured["new_step"] = new_step

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that new_step is forwarded to persistence.

This mock now captures new_step, but the test never checks it. ReviewProcessor could stop passing scheduler_output.step to add_review_and_update_card() and this regression would still pass.

Suggested fix
     assert captured["review"].card_uuid == card.uuid
     assert captured["review"].rating == 3
     assert captured["review"].next_due == scheduler_output.next_due
     assert captured["new_card_state"] == scheduler_output.state
+    assert captured["new_step"] == scheduler_output.step
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_scheduler.py` around lines 527 - 530, The test helper
add_review_side_effect now captures new_step, but the assertion never verifies
it, so a regression in ReviewProcessor forwarding scheduler_output.step to
add_review_and_update_card() would go unnoticed. Update the test around
add_review_side_effect/captured to assert that the persisted new_step matches
the expected scheduler output step, alongside the existing review and
new_card_state checks.

@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.30435% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
flashcore/db/database.py 77.77% 2 Missing ⚠️

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #58      +/-   ##
==========================================
- Coverage   93.24%   93.23%   -0.02%     
==========================================
  Files          24       24              
  Lines        2133     2143      +10     
==========================================
+ Hits         1989     1998       +9     
- Misses        144      145       +1     
Files with missing lines Coverage Δ
flashcore/cli/_review_all_logic.py 98.63% <ø> (ø)
flashcore/cli/review_ui.py 100.00% <100.00%> (ø)
flashcore/db/db_utils.py 84.61% <ø> (ø)
flashcore/db/schema.py 100.00% <ø> (ø)
flashcore/models.py 100.00% <100.00%> (ø)
flashcore/review_processor.py 100.00% <100.00%> (ø)
flashcore/scheduler.py 97.70% <100.00%> (+1.27%) ⬆️
flashcore/db/database.py 80.32% <77.77%> (-0.20%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ImmortalDemonGod
ImmortalDemonGod force-pushed the fix/fsrs-datetime-fidelity branch from 5b1a065 to 734de6f Compare June 29, 2026 20:34
@ImmortalDemonGod
ImmortalDemonGod force-pushed the fix/fsrs-datetime-fidelity branch from 734de6f to f49ed7e Compare June 29, 2026 20:49
@ImmortalDemonGod
ImmortalDemonGod merged commit 8272a00 into main Jun 29, 2026
15 checks passed
@ImmortalDemonGod
ImmortalDemonGod deleted the fix/fsrs-datetime-fidelity branch June 29, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant