Repository navigation
Exchange: zone first, the measured all-day order, no zone switch that drops exceptions - #122
Merged
Merged
Conversation
…es, the rule last A new StartTimeZone keeps an item's stored wall clock and relabels it in the new zone (live round 1, B2; the 8a live test, step 9). The update builder wrote the zone last, so every update that changed an Exchange item's clock moved it by the offset: a single created in Aperio made a series (Monday 00:30 in Berlin landed on Sundays at 23:30), an all-day series given a time of day, and the zone picker on a stored series, which keeps the instant. - The zone goes first whenever it is written (the series' zone, its all-day flag or its slot changes); the rule no longer opens that gate. - Where the written zone names another clock than the stored one, on either boundary and compared as the read side maps an id, Start, End and IsAllDayEvent follow it even where they did not change (decision 240), with the server's values where the edit kept them (106). - The rule comes last, built on the new zone's clock where the clock moves, and is written only when its built form differs from the server's (decision 241): a zone change alone writes no rule and spares the series' exceptions. A blind write still writes or deletes the rule, and an edited rule that no longer builds still fails the save. - The zone and the rule's first day are read from the slot the server keeps after the update, so a kept all-day flag never gets a zone (46a) and a rule never starts on this device's stale start. Tests for every case, eleven red proofs, an adapter-level test of the recorded request; docs (ews.md, DESIGN, TODO, the troubleshooting guides) and the comments that described the old order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a zone change rewrites - Where the edit keeps the server's slot, the edit's rule starts on that kept slot's first day, which a write without the copy cannot know: a third exception to the subset invariant. The test passed only because its synthetic all-day start fell on the same day on every clock it ran on. The exception is named in the `_on` doc and exempted in the test against the rule built on the server's slot, on the same device clock as the code. - A zone change writes no rule only while the series' first day and weekday stay the same on the new clock; near midnight it rewrites the rule. Whether Exchange keeps the exceptions when the slot or the rule is written again is measured by the live test, not claimed (ews.md, DESIGN). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… change risks - Where the edit keeps the all-day flag of a server item that is timed, the zone is the one a timed item takes, read from the kept slot like the rule: a fourth exception to the subset invariant, now named in the `_on` doc. The subset matrix gained an all-day series with a zoned rule and a longer series, with the exemption, and a dedicated test pins the mirror of the kept all-day flag: a stale all-day copy over a timed server series gets the zone and the server's timed slot after it. Red proof: the zone read from either flag fails it. - The rule comparison's comment and a test doc no longer say a zone change spares the series' exceptions. - DESIGN "Ungeprüft": with the zone always first, whether Exchange applies a rule before a later zone no longer matters; 234 itself stays inferred. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o zone switch that drops exceptions The zone-first live test measured what #122's first order could not know: - An all-day daily series given a time refuses the zone first whole (ErrorOccurrenceTimeSpanTooBig, L2). Flag and slot, zone, slot again is accepted but lands a day late (M1, M2); with the rule last, on the new zone's day, it lands right (M7, M8). The writer now sends exactly that shape when the server's item is all-day and the edit is timed, the rule always (decision 244). ErrorOccurrenceTimeSpanTooBig is a whole-request refusal from now on. - Exchange drops every changed and deleted occurrence when a master's Start and End are written (L3a, L3b, M3, M6); a title or a COUNT keeps them (M4, M5). Where Start and End are written only because the zone moves the clock, on a series that has any, nothing is sent: a new WriteRefusal, exceptions-would-be-lost, carried as Forbidden, with its own sentence in German and English on both surfaces (decision 245). A save that moves the series writes them as before; asking first is decision 243, the next PR. Tests for both, an adapter test that nothing is sent, five red proofs; the subset invariant names the server's rule an all-day series takes along. Docs: ews.md, DESIGN (243-245 and the live results), TODO, the troubleshooting guides (a new section for the refusal). Also the third check's two wording fixes (the Feldreihenfolge risk, the clock_moves comment). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…clock, and the live results - No editor picks a series' zone yet (stages 11 and 12), so the new troubleshooting section described a choice users cannot make. Rewritten in both languages as "Exchange would drop a series' changed and deleted occurrences": when the message comes today (a repeat change where Aperio would have to write another zone than the stored one), what to do, and that moving such a series still loses the occurrences until 243 asks first. - Clocks are compared by the zone Exchange stores, so Paris (Romance Standard Time) is another clock than Berlin though it shows the same time: refused on a series with exceptions. A test pins it; the docs no longer promise that a zone with the same time of day saves; a TODO flag for the picker. - DESIGN: the open questions the live test answered are gone, the ones still open named; the Exchange paragraph says only a zone-only rewrite is refused; M6 and L3a/L3b credited to the right runs; round 4 recorded (N1, N1b, N2, N3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for L2, and round 5 - The troubleshooting guides named only a repeat change, but Aperio writes the repeat again when a series is changed or deleted from one of its appointments on, too (it shortens the old series). Both languages now say so, and the advice covers it: do those in Outlook; title, place and reminder of the whole series or of one appointment still save. - L2 measured the zone first refused on a DAILY all-day series; a weekly one took it (round 2, B4). ews.md, the guides and DESIGN 244 say "daily", and that every all-day series gets the measured order all the same. - DESIGN and the rule comment named only COUNT as measured; the UNTIL cut of the 8a live test kept a deleted occurrence. What stays unmeasured is said. - Round 5 recorded: an all-day series Outlook stored in W. Europe, given 10:00, lands right, daily and on Saturdays (O1, O2). - Two tests: O1's request, and a COUNT change or UNTIL cut refused on a series whose stored end zone is not its start zone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The zone gate, the slot a changed rule brings along, and the blind or unbuildable rule write all asked whether the edit touches Recurrence, and the core counts a series' exceptions as part of it. An update never writes exceptions (a deleted or changed occurrence is an item of its own), so a copy that differs from the server only in them — a stale one, once another occurrence was deleted, with nothing proven kept — opened the zone gate: on W. Europe storage a rename wrote the zone too, and where the stored end zone is not the start's, decision 245 refused the rename although the guide says a title still saves. The writer now asks whether the rule's text or zone changes (rule_changed). Such a save writes the title alone, the shape L4a measured. Test and red proof. The guides now also say: if the series was changed elsewhere, open it again once Aperio has refreshed the calendar, and try again. DESIGN 245 and ews.md name the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
The fix the 8a live test called for (step 9): an Exchange update writes the zone first, the slot after it where the clock moves, and the rule last. An all-day series given a time writes the slot on both sides of the zone and the rule always, and a zone switch that would drop a series' exceptions is refused. Decisions 240-242, 244, 245.
Why
A new StartTimeZone keeps the item's stored wall clock and relabels it in the new zone:
The update builder wrote the zone LAST. So every update that changed an Exchange item's clock moved it by the offset:
main behaves the same; this is not a regression of 8a.
What changes (adapter-ews,
event_to_update_field_xml_in)boundary_zone), soW. Europe Standard Timeand a storedEurope/Berlin, or Vienna, which Exchange stores under the same id, are one clock. A stored zone Aperio cannot read, or a missing copy, counts as another clock. A slot written only for this is the server's (landed), so a boundary another device has moved and this edit kept is not put back (106).ErrorOccurrenceTimeSpanTooBig, L2; a weekly one took it, round 2 B4, and gets the same order); without the rule the series lands a day late (M1, M2); this shape lands right (M7, M8).ErrorOccurrenceTimeSpanTooBigis now a whole-request refusal (server-refused), so a split knows nothing landed.exceptions-would-be-lost: zone, carried as Forbidden, with its own sentence on both surfaces ("Diese Änderung würde die Zeitzone der Serie wechseln, und dabei verwirft Exchange ihre geänderten und gelöschten Vorkommen. Es wurde nichts geändert."). A save that moves the series writes them as main does; asking first there is decision 243, the next PR.Creates are unchanged: CreateItem sets everything at once.
Tests
event_to_update_field_xml_in, Berlin device unless noted:Romance Standard Time, another clock): refused asexceptions-would-be-lost: zone; Vienna (the same id) writes the zone alone; a switch that also moves the series writes zone and slot;Europe/Berlin(throughStoredZones::of) with a rule change writes the zone and the rule; no slot in either;each_field_is_written_exactly_when_it_changed(a new "zone only" case) anda_rule_change_carries_the_zone_even_when_the_time_stands_stillnow run on W. Europe storage; the subset invariant allows the server's slot that follows the zone, and the server's rule an all-day series given a time takes along; the 234 test became step 9's.exceptions-would-be-lost: zone, and no UpdateItem sent.WriteRefusal: the new refusal round-trips; the list of refusals read back now holds all nine.src/state/eventWriteError.test.ts): the refusal, from the desktop and from behind the phone's wrapper, reads as its German sentence and as nothing written; its reason fits the split's sentences.touches(Recurrence)back in place ofrule_changed, which failsa_copy_stale_only_in_its_exceptions_writes_the_title_alone).-D warnings,cargo test --workspace,cargo check -p adapter-ews,cargo xtask ts-types(bindings copied), vitest, lint,tsc, mobiletscandcheck:bindings, both live-test generators, docs build andcheck:links.First check (1b832b2)
Two findings were confirmed:
_ondoc and exempted in the test against the rule built on the server's slot, on the same device clock as the code, so the test no longer depends on the machine.Second check (9f8b7f4)
Six findings were confirmed, four distinct:
_ondoc now says the zone and the rule written over a kept slot are read from it; the subset matrix gained an all-day series with a zoned rule and a longer series, with the exemption; a dedicated test pins the mirror direction.Third check (fixed in 1ec656e)
Two wording findings: DESIGN's "Feldreihenfolge" risk still described the order before this PR, and the
clock_movescomment said the edit's slot follows the zone where it is the server's (landed). Both fixed.Fourth check (fixed in 227b806)
Eight findings were confirmed, all about what the docs and this body say; the code stands:
Ten findings were refuted, among them: a rule change refused on a series whose stored end zone differs from its start zone (the documented 245 behaviour, and a shape Aperio never creates); a blind all-day write still sending the zone first (unchanged blind path); the refusal's wording; N3 writing the same id (true, and now said so).
Fifth check (fixed in 02dad28)
Five findings were confirmed, all about what the docs and this body say:
Two tests the reviewers missed were added although their findings were refuted as defects: O1's request, and the end-zone refusal on a COUNT change and an UNTIL cut. Also refuted: the desktop group-carry dialog showing a refusal token in English (older than this PR, for every refusal; flagged as its own task); a German sentence's style.
Sixth check (fixed in 8d5c1a5)
One finding was confirmed, and it was a code gap: the zone gate asked whether the edit touches Recurrence, and the core counts a series' exceptions as part of it. A copy stale only in its exceptions (another occurrence deleted since it was read, nothing proven kept) therefore opened the zone gate, and where the stored end zone differs from the start, 245 refused a plain rename, although the guide says a title still saves. The writer now asks whether the rule's text or zone changes (
rule_changed), in the zone gate, the slot a changed rule brings along and the blind/unbuildable rule terms. Such a save writes the title alone, the request L4a measured. Test and red proof. The guides add: if the series was changed elsewhere, open it again once Aperio has refreshed, and try again.Refuted: a test gap in
leaves_all_day(the code is right); deleting "this and all following" from a view showing the token raw (older than this PR: the views announce every error raw, already in TODO since 118); the guide not saying that a longer or all-day change also drops occurrences (243's scope); B4 cited for the weekly case; theExceptionsWouldBeLostdoc wording.Seventh check (8d5c1a5)
No finding in the code. One in this body: the red-proof list still said sixteen and left out the sixth check's proof; it now lists seventeen. Refuted: the subset test's exemption for the rule an all-day series given a time takes along hides no wrong block (every matrix cell it fires in was traced).
Live test (decision 242)
A local test program drives Aperio's adapter against Toni's Exchange 2019 and reports the fields of each UpdateItem in order, and what the calendar view shows after it.
Round 1, Aperio's request:
ErrorOccurrenceTimeSpanTooBig. L2b, the flag first (not Aperio's request): refused too.Round 2, requests built by hand:
Round 3, by hand:
Round 4, Aperio's own request at 1ec656e:
exceptions-would-be-lost: zone), nothing sent, the series and its exceptions unchanged.Round 5, Aperio's own request at 227b806: an all-day series as Outlook stores it, already in W. Europe, given 10:00. The zone does not change there, yet the series leaves all-day (244); M7 and N1 measured it from UTC only.
Docs
rule_first_day,read_before,REFUSED_UPDATE_CODES, the round 1/2 generator (historical since this change).The phone needs a fresh
.soand XCFramework.🤖 Generated with Claude Code