Skip to content

fix: preserve yearly streak gap repairs through recalculation - #818

Merged
thomasluizon merged 2 commits into
mainfrom
thomasluizon/ticket-483-yearly-gap-window
Oct 20, 2026
Merged

thomasluizon merged 2 commits into
mainfrom
thomasluizon/ticket-483-yearly-gap-window

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Fixes thomasluizon/orbit-tickets#483.

A yearly gap could not be repaired because the completed predecessor fell outside the streak engine's history. Repair eligibility, the response, and subsequent recalculations now share a 732-day window. Two annual intervals retain the predecessor through the next yearly occurrence.

src/Orbit.Application/Common/AppConstants.cs widens the existing shared constant. HabitScheduleService.cs generates the streak schedule in contiguous slices whose endpoint difference never exceeds the unchanged MaxRangeDays of 366. ComputeStreakAsOf already walks the supplied window without another cap, so it now receives the complete wider schedule. The streak feature explanation documents the same limit. Request and response contracts are unchanged; no migration is required. The build regenerated src/Orbit.Api/openapi.json without a diff.

The service tests replace the yearly refusal case with leap-year, ordinary-year, and every-365-day repair cases. A handler test spends the freeze, reads the real streak response, recalculates immediately, the next day, or at the next annual occurrence, saves the user, and reloads the persisted streak from a separate in-memory context. Schedule tests cover the slice boundaries and unchanged public range cap; a history handler test covers the repaired yearly run and its next completion.

Assumptions

  • Use one fixed 732-day window for every user; rejected the cadence-dependent window because it adds per-user query and policy plumbing to every consumer of the existing shared bound.
  • Preserve the existing inclusive range convention, where to - from may be 366 days; rejected changing MaxRangeDays or its endpoint semantics.

GetStreakHistoryQuery uses the new 732-day window rather than 365 so its chart retains the repaired yearly predecessor. Goal queries, notifications, freeze batch loading, and validator limits retain 365 days.

Blast radius

  • GetGoalMetricsQuery, GetGoalReviewQuery, and GoalReviewTool: verified their filtered habit-log includes take the shared constant. They now prefetch 732 days without extra round trips; these database filters do not pass through the schedule range cap. Their existing handler/tool tests pass.
  • GetStreakHistoryQuery: its seed uses the same constant and its schedule now spans all slices. The new yearly history test preserves both the predecessor and the recent completion.
  • UserStreakService: calculation, repair evaluation, gap discovery, and recalculation all retain the shared bound. New repair and persisted-recalculation cases pass alongside existing daily and weekly boundary cases.
  • RepairStreakGapCommandValidator: its existing count bound follows the constant; the bank, monthly allowance, domain guards, and service eligibility checks still apply. Existing validator cases pass.
  • Also verified GoalProgressReadSyncer, GoalDeadlineNotificationService, and StreakFreezeAutoActivationService use the same wider log-prefetch bound. Automatic freeze evaluation continues through UserStreakService.EvaluateRepair; its service tests pass.

Test evidence

Before implementation:

  1. dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~YearlyGapWhosePredecessorSitsBeyondTheStreakWindow_IsRefused --verbosity quiet: the unchanged test passed, 1/1, with the defect present.
  2. dotnet test tests/Orbit.Infrastructure.Tests --no-restore --filter 'FullyQualifiedName~YearlyGapWhosePredecessorSitsBeyondTheOrdinaryWindow_IsRepairable|FullyQualifiedName~RepairedYearlyGap_RecalculationPersistsTheStreak' --verbosity quiet: all 6 strengthened cases failed before the fix. Repeating with --no-build --no-restore --verbosity normal confirmed null eligibility and rejected handler repairs, rather than fixture failures.
  3. dotnet test tests/Orbit.Application.Tests --filter 'FullyQualifiedName~StreakSchedule_LongRangeIncludesEveryDateAcrossSliceBoundaries|FullyQualifiedName~Handle_RepairedYearlyGap_RetainsPredecessorAndRecentCompletion' --verbosity normal: 4 failed and 1 passed before the fix. The 366-day boundary passed; longer schedules and repaired yearly history failed.

After implementation:

  • dotnet build Orbit.slnx --verbosity quiet: passed, zero errors.
  • Focused Infrastructure run covering StreakGapRepairTests, UserStreakServiceTests, and StreakFreezeAutoActivationServiceTests: 84 passed, including all 6 formerly failing repair cases.
  • Focused Application run covering schedule generation, streak history, gap validation, feature explanation drift, goal metrics, goal reviews, the review tool, and streak goal reads: 151 passed, including all 4 formerly failing schedule/history cases.
  • dotnet test tests/Orbit.Domain.Tests --verbosity quiet: exit 0, 767 passed.
  • dotnet test tests/Orbit.Application.Tests --verbosity quiet: exit 0, 5,230 passed.
  • dotnet test tests/Orbit.Infrastructure.Tests --verbosity quiet: exit 0, 3,776 passed.
  • dotnet test tests/Orbit.Analyzers.Tests --no-build --no-restore --verbosity quiet: exit 0, 40 passed.
  • All commit hooks passed, including formatting, dash checks, timeless prose, root allowlist, and suppression accounting.
Command Exit Result
dotnet build Orbit.slnx --nologo 0 Zero errors
dotnet test tests/Orbit.Domain.Tests --nologo 0 767 passed
dotnet test tests/Orbit.Application.Tests --nologo 0 5,231 passed
dotnet test tests/Orbit.Infrastructure.Tests --nologo 0 3,780 passed

Regression proof used:

dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~RepairStreakGapCommandValidatorTests --nologo

  • Unchanged tests passed with the defect present: 5 passed, exit 0.
  • Strengthened RepairDateCount_RetainsTheOrdinaryLimit failed before the fix because 365 dates were accepted: exit 1.
  • After the fix, the strengthened test passed in focused and full Application runs.

Yearly and Feb 29 repairs survive persisted recalculation. Range-slicing tests pass. Generated OpenAPI files are unchanged.

Manual steps

None.

NEEDS_DECISION: May the eight formatter-required whitespace hunks remain (recommended), since removing them conflicts with the mandatory pre-commit formatter?

pullfrog[bot]
pullfrog Bot previously approved these changes Oct 20, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes: Reviewed all six changed files and traced repair eligibility, persisted recalculation, streak reads/history, and consumers of the shared lookback.

  • Shared lookback: Extends streak history to 732 days so yearly-gap repairs retain their completed predecessor through the next annual occurrence.
  • Schedule generation: Generates contiguous bounded slices without changing the public schedule range cap or recurrence anchors.
  • Regression coverage: Adds annual and every-365-day repair cases, persisted recalculation checks, history continuation, and slice-boundary tests; updates the documented limit.
  • Verification: All 297 focused tests passed across scheduling, history, feature explanations, gap repair, streak calculation, automatic freezes, goal consumers, and validation. The full solution test suite was not rerun during this review.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new critical or high-severity issues found.

Reviewed changes: Reviewed the delta since the previous Pullfrog review, with the complete PR diff and affected consumers as context.

  • Separated the limits: Added StreakRepairWindowDays for the 732-day streak calculation, repair, and history paths while retaining the ordinary 365-day consumer and validator limits.
  • Strengthened boundary coverage: Added Feb 29 repair and persisted-recalculation cases, checked predecessor inclusion at 732 versus 733 days, and pinned the existing repair-request count limit.
  • Verified the changes: All 240 focused tests passed across scheduling, history, validation, feature explanations, gap repair, recalculation, automatic freezes, and selected goal consumers. The full solution suite was not rerun during this review.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 90c5320 into main Oct 20, 2026
23 checks passed
@thomasluizon
thomasluizon deleted the thomasluizon/ticket-483-yearly-gap-window branch October 20, 2026 04:30
thomasluizon added a commit that referenced this pull request Oct 20, 2026
…e sub-habits and referral Pro days (#746) (#829)

* fix: preserve yearly streak gap repairs through recalculation (#818)

* fix: preserve yearly streak gap repairs through recalculation

* fix: isolate yearly streak repair history window

(cherry picked from commit 90c5320)

* fix: expose discounted Stripe billed amount per period (#819)

* fix: expose discounted Stripe billed amount per period

* fix: isolate optional recurring Stripe billing preview

(cherry picked from commit 0460a54)

* Make sub-habits free across every creation path (ORB-231) (#824)

* feat: make sub-habits available to every plan (ORB-231)

* test: track free sub-habits in migration model expectations

* fix: preserve Pro slip alerts when creating free sub-habits

(cherry picked from commit c9d22a6)

* Grant atomic referral Pro days (ORB-22) (#823)

* feat: grant atomic referral Pro days (ORB-22)

* fix: preserve referral transaction and billing context

* fix: preserve bonus Pro access across provider changes

(cherry picked from commit 7b4e9af)

* test: retain combined migration assertions after main carry

* fix: include one-time coupons in billed amount preview (#830)

(cherry picked from commit 377eca5)
thomasluizon added a commit that referenced this pull request Oct 20, 2026
* fix: preserve yearly streak gap repairs through recalculation

* fix: isolate yearly streak repair history window

(cherry picked from commit 90c5320)
thomasluizon added a commit that referenced this pull request Oct 20, 2026
Carry the redesign-adapted fixes from a80cf56 for #818, #819, #823, #824 and #830 without replaying their main implementations. Preserve ordinary merge ancestry and the production keep-list for thomasluizon/orbit-tickets#1579.
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