Skip to content

Fix EndOf off-by-one + add Recurring cycle kind#69

Closed
rymiwe wants to merge 2 commits intomainfrom
fix/end-of-cycle-off-by-one
Closed

Fix EndOf off-by-one + add Recurring cycle kind#69
rymiwe wants to merge 2 commits intomainfrom
fix/end-of-cycle-off-by-one

Conversation

@rymiwe
Copy link

@rymiwe rymiwe commented Mar 17, 2026

Summary

Two changes for 0.1.13:

Fix: EndOf cycle final_date off-by-one (QUAL-6189)

  • EndOf#final_date subtracted 1 period before adding period_count, causing V1E12M to expire at the end of month 11 instead of month 12
  • Removed the - 1.send(period) offset so the notation matches user expectations

Example — V1E12M from Dec 1, 2025:

  • Before: expires Nov 30, 2026 (11 months)
  • After: expires Dec 31, 2026 (12 months)

Confirmed by AFMAN 10-3500V1 A2.3.3: "For tasks with a minimum frequency determined by months, currency on the task is maintained through the last day of the expiration month."

Added: Recurring cycle kind — R notation (QUAL-6317)

New cycle type for recurring windows anchored to a from_date.

V1R24MF2026-03-31 = complete 1 within 24 months from March 31, 2026. After completion, the next window starts from the completion date.

  • Unlike EndOf: no end-of-month rounding
  • Unlike Lookback: anchored to from_date, not sliding from today
  • Dormant-capable (activates with F prefix like EndOf and Within)

Anchoring behavior: Next window starts from the completion date, confirmed by AFMAN 10-3500V1 Table A2.2 — frequencies are measured from task performance date. (See comment for full analysis.)

Test plan

  • All 203 gem specs pass (179 existing + 24 new Recurring specs)
  • Qualify integration tested (551 aggregate specs, 21 EndOf specs, 5 domain specs — all pass)

Fixes QUAL-6189
Addresses QUAL-6317

EndOf#final_date subtracted 1 period before adding period_count,
causing V1E12M to expire at end of month 11 instead of month 12.
Remove the offset so the notation matches user expectations:
V1E12M from Dec 1 now correctly expires Dec 31 (not Nov 30).

Fixed: EndOf cycle expiration was 1 month early (QUAL-6189)
@rymiwe rymiwe marked this pull request as ready for review March 17, 2026 04:54
New cycle type for recurring windows anchored to a from_date.
V1R24MF2026-03-31 = complete 1 within 24 months from March 31, 2026.
After completion, next window starts from the completion date.

Unlike EndOf: no end-of-month rounding.
Unlike Lookback: anchored to from_date, not sliding from today.

Added: Recurring cycle kind with R notation (QUAL-6317)
@rymiwe rymiwe changed the title Fix EndOf cycle final_date off-by-one Fix EndOf off-by-one + add Recurring cycle kind Mar 17, 2026
@rymiwe rymiwe requested review from jdowd and saturnflyer March 17, 2026 05:03
@rymiwe
Copy link
Author

rymiwe commented Mar 17, 2026

Open Question Resolved: Next Window Anchors from Completion Date

Reviewed AFMAN 10-3500V1 (3 June 2025), Attachment 2 to confirm the Recurring cycle's anchoring behavior.

A2.3.3: "For tasks with a minimum frequency determined by months, currency on the task is maintained through the last day of the expiration month."

Table A2.2 frequencies (12 mos, 24 mos, etc.) are measured from when the task was performed. No mention of fixed calendar windows for general tasks. Further confirmed by Note 2 (p.34): "if member receives an Unsatisfactory on 15 April, then member's due month is July" — window starts from event date.

The one exception is NRP Recertification (Note 5) which uses a fixed calendar window ending 31 March — handled by the existing Calendar (C) cycle kind.

Decision: Next window starts from the completion date as currently implemented. This PR is ready for review.

@rymiwe rymiwe requested review from a team and removed request for jdowd and saturnflyer March 17, 2026 05:31
Copy link
Contributor

@jdowd jdowd left a comment

Choose a reason for hiding this comment

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

@rymiwe This should be 2 PRs.

I think the change to EndOf is a good one. It may open up a disconnect with users, though. JTACs talked about their Evaluations being due on an 18m cycle but meant "end of 17th subsequent month". I think I'd rather have that disconnect than the one you just fixed, which had always bugged me.

[EDIT] If SOFJTAC ever gets turned back on we'll need a data migration to fix all of the JTAC EndOf cycles, which will now be wrong per their instructions.

I also like Recurring, but a few thoughts:

  • The current name collides with the existing Cycle#recurring? predicate

  • How are you shifting the FROM date, once the cycle is satisfied? If that's the responsibility of the consuming app than you may not need this class at all...you might be able to have the consuming app just use Within, and then move the From date. But then consuming app would be starting to encroach on SOF-cycle's responsibilities...
    https://github.com/SOFware/sof-cycle/blob/main/lib/sof/cycles/within.rb#L5-L40

  • If you keep Recurring you should improve the disambiguation with Within. E.g. their #examples and #to_s behavior should surface their differences.

You also should be able to get the dormant behavior you're looking for by modifying the Parser rather than dormant-checking here. See

def self.dormant_capable_kinds = %w[E W]

I don't love that the dormant-capability is only revealed in Parser, but we should have a single pattern for this...stick with the existing one for Recurring or refactor the approach for all.

Ping me today to discuss if you get a chance.

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.

2 participants