Repository navigation
Exchange writer: a series starts on its first day, an all-day day on the stored zone's midnight (PR 8a, 231-239) - #121
Merged
Merged
Conversation
… (232) StartTimeZone and EndTimeZone carry the zone's full definition. A walker reads it in both item parsers, onto its own boundary; the flag that took a self-closing start zone's place for the end zone's children goes. The new zone_definition module turns a definition into a clock (periods, yearly changes, eras from absolute transitions) and finds a day's midnight, the first hour after it where a change skips it. A fifth weekday reads as the month's last (238). Not wired into reading or writing yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DayZone names the clock an all-day boundary is a midnight in: a zone the CLDR table maps, an id that is tzdata's own name (Europe/Berlin, 236), or a zone Exchange defines in full (232). The end reads in the end zone (233). all_day_anchor_in takes the device clock, so tests pin one. ITEM_PARSER 5 drains every folder once to cache the definitions; READ_RULE 4 re-emits what was read before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the stored zone's midnight (date bug, 47a, 233-235, 237) rrule_to_ews_recurrence takes the series' first day instead of an instant: the device's day for an all-day series, the written zone's day on a create, the stored start zone's day on an update (234), UTC where none is known. Its StartDate, default weekday, day and month come from that day; read off the UTC date they named the day before east of UTC (R3-6). An update writes an all-day day as midnight in the zone Exchange stores each boundary in (47a, 233). read_before returns those zones with the copy; an exception takes its series' ids and definitions. With a zone Aperio cannot read the write is refused as day-zone-unreadable (237), carried as Forbidden; without the copy, the read's own error stands. The builders take the device clock, so tests pin one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n both surfaces Writer tests pin the device clock: an all-day series starts on its day in Berlin, Tokyo, Auckland, Honolulu and New York; a zoned series on its wall clock's day; a zoneless one, or one in a zone the server cannot store, on the UTC day; a rule-only edit carries the right StartDate and a title-only save writes no rule; an all-day day moves on the stored zones' midnights, each boundary in its own, a custom zone by its definition; an unknown zone is refused and a title still saves. Through the adapter: a custom zone's day moves, an id-only one is refused as Forbidden with nothing sent, and without the copy the read's own error stands. The new refusal gets its German and English sentences (237) and its place in eventWriteError.ts; the clippy finding in the duration parser is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…initions, the refusal ews.md says how a series starts on its first day, how an all-day day is written on the stored zones' midnights, how zone definitions are read, and when the day is refused. DESIGN-series-time-zone.md marks 47a and the Exchange half of stage 5 built and lists what 8a leaves unmeasured. TODO flags the series written before 8a (231), Microsoft 365's date bug and the zone-and-rule update that moves a timed appointment. The troubleshooting guide (German and English) explains the new sentence. The round-3 generator expects StartDate 19 for R3-6 now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne exception A zoned timed series whose rule alone changes starts on the day its stored zone reads (234). An exception of a series in a custom zone moves on the series' definition, which rides along to the exception's copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, a bounded bias, and the wording A written slot takes a kept rule along whenever it moves the first day, also when only the all-day flag flips under the same instant: the two read the day on different clocks. A change at 1 January 00:00 opens the year on its bias instead of skipping its first hour (Moscow 2014). A bias beyond a day is refused instead of overflowing. The German sentence has Toni's semicolon again (237). 234 and 233 are named decisions, not measurements, and live step 9 looks at 234. The guides no longer name the iPhone as known good, the fixtures are marked synthetic, the 18 Europe/Berlin items are timed singles, the 217 test has its doc comment back, and TODO says old series carry the day before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r mapped ids, the live steps - An all-day day withheld because the copy could not be read reached the user as a network error, "may have reached the server", and a split would not take its new part back. It is now its own refusal, copy-unreadable, with the read's error as its detail, carried as Forbidden: nothing was sent. German and English sentences, the troubleshooting guides, and a test for HTTP 503 beside the answer without the item. - The parser kept a zone definition beside an id the table maps, so the cache carried two definitions for every Outlook item. It now keeps one only for an id that names no clock Aperio knows. - Round 3's generator steps are marked historical: what round 3's build sent, and the 47a prototype as Aperio's rule since 8a. Red proofs: copy-unreadable left out of the Forbidden mapping, and the definition kept for a mapped id, each fail their test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one items, the generator and live step 6 - A zoned timed series updated without the copy still started its rule on the UTC date, while the same request wrote the series' zone: Monday 00:30 in Berlin went out as StartDate Sunday. Without the copy the rule now starts on the day of the zone the update writes with it, as a create does. - copy-unreadable hid a failed sign-in or a deleted item behind "try again". A read that failed on the sign-in or found the item gone keeps its own error (Authentication, NotFound), as the same edit of a timed appointment reports it. `names_sign_in_or_gone` shares its SOAP codes with `to_core_error`, and a test checks the two agree. - Round 3's generator says what still differs from round 3 (the rule's StartDate in R3-1 and R3-2) and names R3-7b as the measured 47a step. - The 234 test comment states a decision, not a measurement. - ews.md: the blind rule clock and the errors that stay themselves. Red proofs: the blind clock taken out, and every read error wrapped as copy-unreadable again, each fail their test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng tested - When the copy could not be read, the adapter logged "writing every field not kept" before it knew whether the update would go out. It now logs the failed read, then the outcome: the all-day day withheld (with the reason) or the blind write that goes out. A day refused in a zone Aperio cannot read is logged as well. - Decision 233 through the production paths: an adapter test reads a Greenwich start and a W. Europe end from the server copy and writes each on its own zone's midnight; a read test takes a Tokyo start and a Honolulu end through to_event and both override_event paths. - The update builder's comment and DESIGN-series-time-zone.md name the rule clock of an update without the copy. Red proofs: the end zone taken from the start zone in StoredZones::of, in to_event, in an exception's own row and in an inherited row, each fail their test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d's generator note, German reasons
- "writing without the current copy" is logged only when an update goes
out: it now follows the empty-update check.
- ews.md's Testing paragraph says round 3's generator is historical since
PR 8a and which of its files are Aperio's rule now.
- A test comment names all_day_boundary, which replaced
ews_all_day_boundary.
- The German reasons of day-zone-unreadable and copy-unreadable stand only
in the split's sentences about "die alte Serie", so they take its gender
("sie", "ihr Tag", "ihren Stand"). The test asserts both whole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erator's doc as history - The update builder looked the written series zone up twice on a blind update (once for the blind clock, once for the zone it writes), so a zone Exchange cannot store was logged twice. It is looked up once and used for both, as the create does. - The round 3 generator's doc opened with the old "NOT Aperio's rule" list; it is now history, and ews.md names the one field that differs from round 3 (the rule's StartDate in R3-1 and R3-2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its target The zone walker kept attribute ids as written, entities and all, while both item parsers decode a <To> target's text. A period or group id holding '&' then never matched its target, and a complete definition was refused as unreadable (237). Attribute values are now decoded (`attribute_text`), in the walker and for the StartTimeZone/EndTimeZone ids, so a zone id has one form whichever element names it. ITEM_PARSER 5, introduced by this PR, drains every folder once anyway. Red proofs: the walker's ids, and the zone elements' ids, read as written again; each fails the new test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Steps 1 to 7 of the live test passed: the date bug is fixed, all-day days move on the stored zones' midnights, Exchange 2019 sends a custom zone's definition in GetItem and SyncFolderItems and the day moves on it, and a zone named Europe/Berlin reads as Berlin. Two results refute what the code assumed, both the same on main: - A timed "this and all following" split shows the cut day twice: the old series ends on the date of its UNTIL, and Exchange counts the EndDate in (235). Its own PR follows. - A single made weekly in one save lands on Sundays at 23:30 instead of Mondays at 00:30: the rule starts on the stored zone's day (234), and the new zone written after it keeps the stored wall clock. 234 is refuted; the next PR writes the zone first. DESIGN (stage 4, "Gemessen" and "Ungeprüft"), TODO, ews.md and the comments that cited the live steps say so now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Step 9 does not refute 234: the update sent the rule and then the zone, without the start or the end, and Exchange applied them in that order, as 234 assumes. The zone without the start and the end kept the stored wall clock. The next PR writes the zone, then the start and the end even when unchanged, then the rule on the new zone's day, which replaces 234. - "Round 6" already names an iCloud round; this is "the 8a live test". - Step 6 used items the test program created through EWS; an iPhone or invitation item and the 237 refusal stay unmeasured. Step 7's item was created the way Outlook stores it, not by Outlook. - The repeat-only change of step 5 is the first-day rule, not 47a. - Measured as well: a W. Europe series reads back with offset dates (2026-11-04+01:00), and Exchange 2019 sends a custom zone's definition, which the zone_definition module doc now says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rder Step 9 neither confirms nor refutes 234. The rule named Sunday itself, and a zone that keeps the wall clock keeps its day, so either order of the rule and the zone gives Sundays at 23:30. What it measured: the new zone, sent without the start and the end, kept the stored wall clock. 234 stays inferred from round 1 (DESIGN, TODO, ews.md and rule_first_day say so again); the next PR measures whether zone, start and end, then the rule on the new zone's day lands. Two fixture comments said no definition had been captured from a server; the live test captured Exchange echoing one the test wrote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he measured offset Three comments in mapping.rs (the EndDate arm of to_rrule, range_date and its test) still called an offset only the schema's other form. The 8a live test measured +01:00 on both dates of a W. Europe series; a west offset stays unmeasured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…list 8401281 took 234 off DESIGN's "Ungeprüft" list on the reading that step 9 refuted it; that reading is withdrawn, and step 9 cannot show the order of a rule and a zone. It is back on the list. An older TODO line still called Exchange's EndDate inclusivity unmeasured; the live test measured it (235). 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.
PR 8a of the arc "exceptions when splitting a series": the Exchange writer, with 47a and the date bug together, as decision 57a set out. It lands before PR 8, because PR 8 finds each deleted occurrence by its slot, and the date bug shifted Exchange's series by a day. Decisions 231-239.
Why
rrule_to_ews_recurrencetook the range's StartDate from the UTC date of the start. It also took the weekday, day of the month and month that a rule without BYDAY, BYMONTHDAY or BYMONTH needs from there.What changes
A series starts on its first day.
rrule_to_ews_recurrence(rrule, first_day: NaiveDate)takes the day the series starts on, on the clock Exchange repeats it on (rule_first_day):The comparison of an update builds both sides on the same clock, so a title-only save writes no rule. A written slot takes a kept rule along whenever it moves the first day, including when only the all-day flag flips under the same instant.
An all-day day goes on the stored zones' midnights (47a).
all_day_boundary). Exchange rounds an item with one stored zone in that zone (R3-5b-u). That each boundary uses its own zone when the two differ is decision 233, which R3-2 suggests; nobody has measured it.read_beforereturns the zones with the copy (StoredZones). An exception takes its series' ids and definitions, because its read shape asks for no zone.Which zones Aperio can read (232, 236, 239). An all-day boundary reads, and writes, in:
Europe/Berlin. 18 of Toni's items carry that id, all of them timed singles in a 15 September snapshot; it counts for one made all-day.Customized Time Zonein Toni's mailbox, where Exchange sends the definition. Exchange 2019 sends it inGetItemandSyncFolderItems, measured on an item the live test created through EWS with a definition (step 6); an item from an iPhone or an invitation is unmeasured. The fixtures are synthetic.A definition is kept only where the id names no clock Aperio knows. For a mapped id the table's zone wins, so the cache holds no definition for it.
The new
zone_definitionmodule does two things:ZoneRulesturns a definition into a clock from its periods, its yearly changes, and the eras set by absolute transitions. From the clock it gives the wall time at an instant and a day's midnight. Where a change skips midnight, the midnight is the first hour after it. Windows' 1 January 00:00 marker opens the year on its bias instead of skipping the first hour. A fifth weekday reads as the month's last (238). A bias beyond a day is refused.The read anchors with the same zones (
all_day_zone,all_day_end_zone,all_day_anchor_in).Bumps.
Refused, not guessed (237). Where a boundary's zone cannot be read:
day-zone-unreadable, carried asForbidden, so the phone keeps the sentence and a split knows nothing landed.copy-unreadable, with the read's error (or "the server answered without the item") as its detail. It is carried asForbiddentoo: nothing was sent. A read that failed on the sign-in or found the item gone keeps its own error (Authentication,NotFound), as the same edit of a timed appointment reports it: a retry would not help.Both tokens have German and English sentences in
eventWriteError.tsand a section in the troubleshooting guide.Testability. The builders take the device clock (
_in), so tests pin Berlin, Tokyo, Auckland, Honolulu and New York on any machine. In chrono 0.4.44,Localon Windows never reads TZ.Not in this PR
Tests
to_eventandoverride_event);TimeZoneDefinitiongives its id;&read the same in an attribute and in a<To>target, and the zone's own id is decoded.ZoneRules: W. Europe including the change days, the order of a group's changes, Sydney, Chile's skipped midnight, India, Kiribati, Microsoft's Eastern sample with its 2007 rule change, a fifth weekday, named errors, every duration spelling.copy-unreadablewith nothing sent;ErrorItemNotFound) keeps its own error, with nothing sent;names_sign_in_or_goneagrees withto_core_erroron every code;read_beforeandStoredZones::of);-D warnings;cargo test --workspace, andcargo check -p adapter-ewsalone;check:links;Live test plan (decision 235)
With a desktop build of this branch, in Berlin, on Toni's Exchange; Outlook to check:
StartTimeZonecarriesPeriods, that is, whether Exchange sent the zone's definition. Two outcomes are right:Live test results (2026-10-10)
Steps 1 to 5 by hand on the CI build of ca398e0. Steps 6 to 9 through a test program outside the repository, which drove Aperio's own adapter, created its own items through EWS and read back what Exchange stored and what the calendar view shows; all its items went to Deleted Items afterwards.
GetItemandSyncFolderItems; moved to Wednesday, each lands on its zone's midnights, one day. Passed. An item from an iPhone or an invitation, and the 237 refusal on the real server, stay unmeasured.UNTILone second before the cut, its EndDate is that date, and Exchange counts the EndDate in. A day deleted before the cut stays deleted. main's writer sends the same request; its own PR follows (235).Also measured: a W. Europe series reads back with offset dates (
2026-11-04+01:00) without aTimeZoneContext.The docs now record these measurements: DESIGN (stage 4, "Gemessen", and "Ungeprüft"), TODO, ews.md, the zone_definition module doc, and the comments that cited the live steps.
Review of the live results
A short review (two finders, one skeptic per finding) checked the docs against the test report and the SOAP log. It confirmed 14 findings, all fixed: step 9 does not refute 234 (the zone without start and end keeps the wall clock, so the fix must also resend them); "round 6" already names an iCloud round, so this is "the 8a live test"; step 6 used items the program created, and step 7's item was not made by Outlook; the repeat-only change in step 5 is the first-day rule, not 47a; the EndDate offset and the definition are now measured; the stale sentences above.
A second short review confirmed six more, all fixed: step 9 cannot show the order of a rule and a zone either, so it neither confirms nor refutes 234, which stays inferred (DESIGN, TODO, ews.md,
rule_first_dayand this body now say so); and two fixture comments still said no definition had been captured from a server.A third short review confirmed one: three EndDate comments in mapping.rs still called an offset only the schema's form; they now name the measured
+01:00.A fourth confirmed one: 234 had left DESIGN's "Ungeprüft" list on the withdrawn "refuted" reading; it is back. An older TODO line now records EndDate as inclusive.
First check (93456d9)
Six finders, one skeptic per finding. Nine findings were confirmed and fixed:
BadBias.Europe/Berlinitems are named as timed singles.Red proofs:
A 31 December marker had no effect on any midnight, so that handling was taken out again. Thirteen findings were refuted.
Second check (cbfae15)
The review ran again over the first check. Five findings were confirmed:
copy-unreadable, carried asForbidden, with its own sentence.Red proofs:
copy-unreadablemapped toNetworkinstead ofForbidden;Third check (02e46df)
Five findings were confirmed:
AuthenticationorNotFound, which the desktop counts as nothing written, as before this PR.Red proofs:
copy-unreadableagain.Fourth check (fbd2591)
Four findings were confirmed, two of them the same:
copy-unreadable, or the sign-in or gone error) with the reason, or the blind write that goes out. A day refused in a zone Aperio cannot read (day-zone-unreadable) is logged too. These lines have no test: adapter-ews has no log capture.to_eventand bothoverride_eventpaths.event_to_update_field_xml_incomment and DESIGN-series-time-zone.md.Red proofs, each failing its test:
StoredZones::of;to_event;Fifth check (bea6fc0)
Three findings were confirmed, plus one a verifier set aside:
ews_all_day_boundary, which this PR replaced withall_day_boundary.Sixth check (9b05c11)
Two findings were confirmed, both small:
Seventh check (ca398e0)
One finding was confirmed. The reviewer also checked
ZoneRulesagainst tzdata (Berlin, Sydney, Auckland, New York, Lord Howe, Chatham, the Azores, Havana, Cairo, a fifth weekday, Moscow's 2009-2016 eras and São Paulo's 2018-2020 eras): midnights,instant_ofandwallat every half hour, with no mismatch.&never matched its target. The walker kept attribute ids as written (&), while the parsers decode a<To>target's text, so such a definition was refused as unreadable. Attribute values are now decoded (attribute_text), in the walker and for theStartTimeZone/EndTimeZoneids, so a zone id has one form whichever element names it. ITEM_PARSER 5, which this PR introduces, drains every folder once anyway.Red proofs: the walker's ids, and the zone elements' ids, read as written again; each fails the new test.
Docs
day-zone-unreadable,copy-unreadable);The phone needs a fresh
.soand XCFramework.🤖 Generated with Claude Code