Skip to content

feat(flex): read path anchors on the FlexChainComputedThrough cursor - #1701

Merged
renemadsen merged 1 commit into
stablefrom
feat/read-path-anchors-on-cursor
Sep 2, 2026
Merged

feat(flex): read path anchors on the FlexChainComputedThrough cursor#1701
renemadsen merged 1 commit into
stablefrom
feat/read-path-anchors-on-cursor

Conversation

@renemadsen

Copy link
Copy Markdown
Member

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.FlexChainComputedThrough records 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

FlexChainComputedThrough is 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 traced dbAssignedSite back to its load sites to confirm FlexChainComputedThrough is 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 of UpdatePlanRegistration, and the Index / IndexByCurrentUserName read endpoints. All six already had dbAssignedSite in scope, so nothing needed plumbing through.

Known gap, deliberately not closed here

The same anchor shape exists unamended on write and import pathsTimePlanningWorkingHoursService (CreateUpdate, CreatePlanning, both UpdateWorkingHour overloads, Import) and GoogleSheetHelper. 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

…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).
Copilot AI lite review requested due to automatic review settings September 2, 2026 15:30

Copilot AI 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.

🟡 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 FlexChainComputedThrough predicate 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);

@renemadsen
renemadsen merged commit 35b4ceb into stable Sep 2, 2026
76 of 78 checks passed
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