feat(flex): read path anchors on the FlexChainComputedThrough cursor - #1701
Merged
Conversation
…ursor Adds PlanRegistrationHelper.ResolveChainAnchor, expressing "seed from the latest row strictly before the window that the site's cursor vouches for" -- a row created ahead of time and never filled in (SumFlexEnd 0) can no longer be mistaken for the chain's true predecessor once its date passes. A null cursor (every tenant today, since the column is published but not yet backfilled) falls back to the exact prior behavior: latest row strictly before the window, seed from zero if none. Amends the six live preTimePlanning anchor queries (four in PlanRegistrationHelper, two in TimePlanningPlanningService's Index and IndexByCurrentUserName) with the same predicate so EF filters at the database, matching the in-memory helper. Closes out the flex-chain-continuity plan (task 7 of 7).
There was a problem hiding this comment.
🟡 Changes recommended
The newly added ResolveChainAnchor helper can return an anchor for the wrong site because it does not filter candidates by the provided site.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates TimePlanning’s flex-chain “anchor row” selection during reads/recompute so that it won’t seed from pre-created (potentially uncomputed) future rows, by constraining eligible anchor candidates to rows at or before AssignedSite.FlexChainComputedThrough when present.
Changes:
- Added
FlexChainComputedThroughpredicate to six “previous PlanRegistration” anchor queries to avoid seeding from uncomputed zero rows. - Introduced
PlanRegistrationHelper.ResolveChainAnchor(...)helper documenting the intended anchor-selection semantics. - Added unit tests covering cursor vs. null-cursor anchor selection.
File summaries
| File | Description |
|---|---|
| eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs | Adds cursor-aware filtering when selecting the predecessor row used to seed missing-day recompute rows. |
| eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Helpers/PlanRegistrationHelper.cs | Adds a documented anchor resolver helper and applies the cursor-aware predicate to additional recompute anchor queries. |
| eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationHelperTests.cs | Adds tests validating anchor selection behavior with and without a cursor. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+374
to
+378
| public static PlanRegistration? ResolveChainAnchor( | ||
| IEnumerable<PlanRegistration> candidates, AssignedSite site, DateTime windowStart) | ||
| { | ||
| var eligible = candidates.Where(x => x.Date < windowStart); | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Final piece of the flex chain continuity design. Requires base 10.0.62.
What it closes
A read currently anchors its chain recompute on the latest row before the requested window — whatever that row happens to be. Rows are pre-created up to 180 days ahead holding zeros, which is correct when written since future rows carry no flex. If nothing computes them once the date passes, that zero row becomes the anchor and silently discards the accumulated balance.
~240,000 orphaned hours across 90 tenants came from this. On one tenant, 90% of past rows were created more than 30 days ahead of their own date, and 71% of the zero-holes more than 120 days ahead.
AssignedSite.FlexChainComputedThroughrecords how far a site's chain is known computed. Anchoring at or before it means a read never seeds from a row nothing filled in.It ships dormant
FlexChainComputedThroughis null on every tenant today — the column is published but not deployed, and nothing populates it until the ops script runs. So the null-cursor path is the only one that executes in production the day this lands.Under a null cursor the added predicate degrades to
TRUE OR x.Date <= NULL, an unconditional match: byte-identical to the query before this change, at all six anchor sites. Review verified each one individually, and traceddbAssignedSiteback to its load sites to confirmFlexChainComputedThroughis genuinely materialized rather than defaulted by a partial projection — a failure mode that would not show in the diff.Scope
Six anchor queries: both branches of
UpdatePlanRegistrationsInPeriod, both ofUpdatePlanRegistration, and theIndex/IndexByCurrentUserNameread endpoints. All six already haddbAssignedSitein scope, so nothing needed plumbing through.Known gap, deliberately not closed here
The same anchor shape exists unamended on write and import paths —
TimePlanningWorkingHoursService(CreateUpdate,CreatePlanning, bothUpdateWorkingHouroverloads,Import) andGoogleSheetHelper. Those remain able to seed from an uncomputed zero row.This is a gap in the plan's scope rather than a defect in this change: the plan targeted the read/list endpoints the bug report described and did not sweep the shape plugin-wide. The exposure is much narrower once the nightly catch-up job runs, since gaps stay about a day — but the bug class is not fully closed, and a follow-up should sweep the remaining call sites.
🤖 Generated with Claude Code
https://claude.ai/code/session_01TgEyDcnAEBcCF63RX2vm1k