Skip to content

fix(plantext): parse through the shared base parser - #1724

Merged
renemadsen merged 1 commit into
stablefrom
fix/plantext-parse-decimal-break-5shifts
Sep 24, 2026
Merged

renemadsen merged 1 commit into
stablefrom
fix/plantext-parse-decimal-break-5shifts

Conversation

@renemadsen

Copy link
Copy Markdown
Member

Blocked on eform-timeplanning-base#906. Pins Microting.TimePlanningBase 10.0.64, not published yet, so CI cannot restore until that merges and v10.0.64 is tagged. Verified locally against a packed copy.

Problem

PlanTextHelper.BreakTimeCalculator was a 25-entry string whitelist whose largest key was "1". Everything longer hit _ => 0 silently, and RecalculatePlanHours then deducted nothing — inflating PlanHours by the full break.

Four copies of that table existed. Two were dead — and one of the dead ones already held the corrected values up to "4.75". The fix had been written against a copy nobody called. The comments in ContentHandoverService pointed readers at that dead method, which is plausibly how the live ceiling survived review.

A sweep of all 227 tenant schemas found ~3,900 live rows losing their break outright: 1.5/1,5 (2,436), 1.0/1,0 (642), 0,50 (372), plain 27, and prose-suffixed tokens like 2 helligdag and ½ + AT.

What changes here

PlanTextHelper and both dead tables are deleted; everything routes through PlanTextParser / PlanRegistrationPlanText in the base, which the background service now shares (service PR).

ContentHandoverService loses ~175 lines of private parsing that disagreed with the live parser on the same input"0.1" was 5 minutes in one and 6 in the other, "1.5" was 0 versus 90 — and whose FormatBreakAsCanonicalHours deliberately emitted values it knew the live parser would drop. Its three PlanText helpers are now internal rather than private, so tests drive them directly via the existing InternalsVisibleTo (as ComputeShiftPauseSecondsTests already does) instead of by reflection.

The handover fallback segment now renders times as 8:00 rather than zero-padded 08:00. That's a fix, not a regression: MinutesToTimeString already wrote 8:00, so the fallback was the sole outlier and handover text disagreed with grid text. They now agree.

Review caught a data-loss bug in the base, fixed upstream

The first version of ParseInto assigned PlanHours whenever PlanText was non-empty. But GoogleSheetHelper assigns PlanHours from the sheet's hours column immediately before parsing, so a row reading Ferie, Helligdag, a bare 8 or 7,4 had its real hours overwritten with zero — which then seeds SumFlexEnd and drifts every following day. The read path runs on every period load for a sheet site, so it wasn't import-only. Fixed in #906: PlanHours is derived only when the text actually described a shift.

Two things NOT fixed here, flagged deliberately

Legacy single-digit-fraction rows have stale columns. The old parser read 8.5 as 08:05; the new one reads 08:30. Rows already store PlannedStartOfShift1 = 485 while PlanText still says 8.5-16. Handover matches segments by start/end, so those won't match, and the sender's columns get cleared while its PlanText keeps the segment — the shift can reappear on the sender and exist on the receiver simultaneously. Wants either a re-derive pass before deploy or a start-only fallback match.

PlanChangedByAdmin may latch on the first admin save. Detection is entry.State == EntityState.Modified, evaluated after PlanText is regenerated. Rows whose break is outside the old 5–60 grid now render differently, so even a no-op save marks the entity modified and permanently severs that day from sheet sync. Moving the snapshot would change admin-change detection semantics — and CalculatePauseAutoBreakCalculationActive runs between the two points — so I have not rewritten that inside a parsing PR.

Tests

PlanTextHelperTests replaced by PlanTextIntegrationTests, registered in the shard filter in both workflow files (this repo filters by class name; an unregistered class silently never runs — ShardCoverageTests will confirm).

It deliberately does not re-test the base grammar, which has ~130 cases of its own. It covers what the call sites assume: non-shift text leaving the hours column alone, shift text owning PlanHours, a regenerated PlanText reading back as the same shift, and handover moving a 90-minute break intact.

🤖 Generated with Claude Code

Deletes PlanTextHelper and both dead BreakTimeCalculator tables in favour
of PlanTextParser / PlanRegistrationPlanText in the base, which the
background service now shares.

One of the dead tables already held the corrected break values up to
"4.75". The fix had been written against a copy nobody called, which is
how the live one-hour ceiling survived review: a break of 1.5 became zero
and PlanHours was inflated by the whole break.

ContentHandoverService loses ~175 lines of private parsing and formatting
that disagreed with the live parser on the same input -- "0.1" was five
minutes in one and six in the other -- and whose own comments described a
ceiling in a table that was never reached. Its three PlanText helpers are
internal rather than private so the tests can drive them directly through
InternalsVisibleTo, as ComputeShiftPauseSecondsTests already does, rather
than by reflection.

The handover fallback segment now renders times the same way the grid
does. It emitted a zero-padded "08:00" where everything else wrote
"8:00", so handover text and grid text disagreed; they now agree, and a
break off the five-minute grid round-trips instead of collapsing.

Tests cover what the call sites assume rather than re-testing the base
grammar: that text which is not a shift leaves the sheet's hours column
alone, that shift text owns PlanHours, that a regenerated PlanText reads
back as the same shift, and that handover moves a 90-minute break intact.

Includes the design note the change was built from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 23, 2026 05:22

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.

Copilot review overview

🟡 Changes recommended

Critical handover compatibility and admin-save flag issues remain unresolved, along with a moderate zero-break formatting inconsistency.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)
What changed in this PR

This PR centralizes PlanText parsing through the shared base parser, fixes extended-break handling, removes duplicate logic, and adds integration coverage.

Changes:

  • Updates the shared parser dependency and call sites.
  • Simplifies handover and helper parsing.
  • Replaces obsolete tests and registers new tests in CI.
  • Documents parser behavior and rollout considerations.
File Summary
eFormAPI/​Plugins/​TimePlanning.Pn/​TimePlanning.Pn/​TimePlanning.Pn.csproj Updates the shared parser dependency.
eFormAPI/​Plugins/​TimePlanning.Pn/​TimePlanning.Pn/​Services/​TimePlanningPlanningService/​TimePlanningPlanningService.cs Uses shared PlanText generation.
eFormAPI/​Plugins/​TimePlanning.Pn/​TimePlanning.Pn/​Services/​ContentHandoverService/​ContentHandoverService.cs Uses shared parsing and formatting for handover operations.
eFormAPI/​Plugins/​TimePlanning.Pn/​TimePlanning.Pn/​Infrastructure/​Helpers/​PlanTextHelper.cs Removes the obsolete local parser.
eFormAPI/​Plugins/​TimePlanning.Pn/​TimePlanning.Pn/​Infrastructure/​Helpers/​PlanRegistrationHelper.cs Routes parsing through the shared implementation.
eFormAPI/​Plugins/​TimePlanning.Pn/​TimePlanning.Pn/​Infrastructure/​Helpers/​GoogleSheetHelper.cs Uses shared parsing during sheet imports.
eFormAPI/​Plugins/​TimePlanning.Pn/​TimePlanning.Pn.Test/​PlanTextIntegrationTests.cs Adds parser integration and handover coverage.
eFormAPI/​Plugins/​TimePlanning.Pn/​TimePlanning.Pn.Test/​PlanTextHelperTests.cs Removes obsolete helper tests.
docs/​superpowers/​specs/​2026-09-22-plantext-parsing-design.md Documents parser behavior and migration scope.
.github/​workflows/​dotnet-core-pr.yml Registers the replacement test fixture.
.github/​workflows/​dotnet-core-master.yml Registers the replacement test fixture.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1516 to +1517
&& PlanTextParser.TryParseSegment(seg, out var shift)
&& shift.StartMinutes == targetStart && shift.EndMinutes == targetEnd)
planning.PlannedBreakOfShift5 = model.PlannedBreakOfShift5;
planning.PlannedEndOfShift5 = model.PlannedEndOfShift5;
planning.PlanText = PlanTextHelper.GeneratePlanText(planning);
planning.PlanText = PlanRegistrationPlanText.Generate(planning);
@renemadsen
renemadsen merged commit 50352a9 into stable Sep 24, 2026
81 of 92 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