Skip to content

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
Timtam merged 19 commits into
mainfrom
feat/ews-writer-47a-date
Oct 10, 2026
Merged

Timtam merged 19 commits into
mainfrom
feat/ews-writer-47a-date

Conversation

@Timtam

@Timtam Timtam commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

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

  • The date bug. rrule_to_ews_recurrence took 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.
    • East of UTC, an all-day series named the day before. Live round 3 (R3-6) measured this: a daily all-day series for Monday 19 October started on Sunday 18.
    • A timed series at 00:30 in Berlin, or one in the evening in New York, named the wrong day as well.
  • 47a. An update wrote an all-day day as UTC midnights without a zone. Exchange rounds a boundary in the zone it stores the item in (R3-5b-u), so an Outlook all-day appointment moved by Aperio came out two days long. Decision 217 had to read around the relabelled midnights this left behind.

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):

  • an all-day series: the device's day;
  • a create: the written zone's day;
  • an update: the stored start zone's day. Decision 234 infers this from live round 1, where Exchange applied an update's fields in order; the 8a live test (step 9) is consistent with it but cannot show the order of a rule and a zone. An update that also changes the zone still moves the item, see the results.
  • an update whose copy could not be read: the zone the update writes with the rule, as on a create.
  • otherwise UTC.

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).

  • An update writes each all-day boundary as midnight in the zone Exchange stores that boundary in, of the day the device names (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_before returns the zones with the copy (StoredZones). An exception takes its series' ids and definitions, because its read shape asks for no zone.
  • A create still writes UTC midnights: Aperio creates without a zone, so Exchange stores the item in UTC.

Which zones Aperio can read (232, 236, 239). An all-day boundary reads, and writes, in:

  • a zone the CLDR table maps;
  • a zone whose id is tzdata's own name, such as 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.
  • a zone Exchange defines in full, such as Customized Time Zone in Toni's mailbox, where Exchange sends the definition. Exchange 2019 sends it in GetItem and SyncFolderItems, 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_definition module does two things:

  • Reading. A walker in both item parsers reads the definition onto its own boundary. It replaces a flag that stayed set after a self-closing start zone. That flag fed only a nested definition's id and never misread an item, but a definition read through it would have landed on the start.
  • Computing. ZoneRules turns 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.

  • ITEM_PARSER 5: every folder is drained once, to cache the definitions.
  • READ_RULE 4: what was read before is emitted again.

Refused, not guessed (237). Where a boundary's zone cannot be read:

  • The day is not written. The update is refused as day-zone-unreadable, carried as Forbidden, so the phone keeps the sentence and a split knows nothing landed.
  • Without the copy, the update is refused as copy-unreadable, with the read's error (or "the server answered without the item") as its detail. It is carried as Forbidden too: 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.
  • A save that leaves the day alone still goes out.

Both tokens have German and English sentences in eventWriteError.ts and 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, Local on Windows never reads TZ.

Not in this PR

  • Series already written with the wrong start day (231) stay as they are. Moving one and choosing its weekday or day again in the repeat picker mends it.
  • Microsoft 365's identical date bug. It gets its own PR later.
  • A zone and a rule in one update. When Aperio turns a timed appointment into a series with a new zone, the new zone keeps the stored wall clock, so the appointment moves by the offset (round 1, stages 9 and 12; the 8a live test, step 9). Pre-existing. The next PR writes the zone, then the start and the end, then the rule on the new zone's day.
  • Timed "this and following". The cut day shows twice, because the EndDate is inclusive (the 8a live test, step 8). Pre-existing; its own PR follows (235).

Tests

  • Writer (devices pinned):
    • an all-day series starts on its day on five devices;
    • a zoned series starts on its wall clock's day;
    • a zoneless series, or one in a zone the server cannot store, starts on the UTC day;
    • rule-only edits, all-day and zoned, carry the right StartDate, and so does a zoned series' update without the copy;
    • a title-only save writes no rule;
    • all-day days move on the stored zones' midnights: W. Europe, Tokyo, devices far away, two zones, UTC unchanged, a custom definition;
    • timed to all-day;
    • an unknown zone is refused while a title still saves;
    • a new rule with a new zone starts on the stored zone's day (this pins what 8a writes; the live test showed the item still moves, and the next PR changes it).
  • Reader:
    • a tzdata name reads as its zone;
    • a custom +14 zone reads its day by its definition, where the UTC sample named the day before;
    • the end reads in the end zone, in a single's row and in an exception's, inherited or its own (233 through to_event and override_event);
    • both parsers put a definition on its own boundary, a mapped id keeps none, and a nested TimeZoneDefinition gives its id;
    • ids holding &amp; 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.
  • Through the adapter:
    • a custom zone's day moves;
    • an id-only one is refused as Forbidden with nothing sent;
    • without the copy, whether the server answers without the item or with HTTP 503, the day is refused as copy-unreadable with nothing sent;
    • a failed sign-in (HTTP 401) or a gone item (ErrorItemNotFound) keeps its own error, with nothing sent; names_sign_in_or_gone agrees with to_core_error on every code;
    • a start stored in Greenwich and an end in W. Europe each move on their own zone's midnight (233 through read_before and StoredZones::of);
    • a custom-zone exception moves on its series' definition.
  • TypeScript: both new refusals read as their sentences, behind Expo's wrapper too, and as nothing written; their German reasons are asserted whole.
  • Red proofs: 14 rules broken one at a time, each fails at least one test:
    • the all-day first day;
    • the timed clock;
    • the update's stored zone;
    • 47a;
    • 233 written and read;
    • 236;
    • 239;
    • the walker's boundary;
    • the exception's definitions;
    • Forbidden;
    • the read's error;
    • 238;
    • the bias sign.
  • Gates:
    • fmt and workspace clippy -D warnings;
    • cargo test --workspace, and cargo check -p adapter-ews alone;
    • desktop tsc, eslint and vitest (2378, also in UTC);
    • mobile tsc and the FFI bridge check;
    • docs build and check:links;
    • both live-test generators.

Live test plan (decision 235)

With a desktop build of this branch, in Berlin, on Toni's Exchange; Outlook to check:

  1. A new all-day series in Aperio, daily from Monday 2.11. to Thursday 5.11. Outlook should show Monday to Thursday.
  2. An all-day series every 2 weeks on Monday, 3 times. Outlook should show 2.11., 16.11. and 30.11.
  3. A timed series in Aperio at 00:30 on Monday 2.11., weekly with no weekday chosen, 3 times. Outlook should show it on Mondays.
  4. An all-day appointment made in Outlook, moved in Aperio from Monday to Tuesday. Outlook should show it on Tuesday only, one day.
  5. All-day series made in Outlook:
    • A weekly Monday series: change its 9.11. occurrence in Outlook, then move that occurrence in Aperio to 10.11. Outlook should show 10.11. as one day.
    • A second weekly Monday series without changed occurrences: change only its repeat in Aperio, to every 2 weeks. It should keep its Mondays, starting on the same day.
  6. An all-day appointment created in the iPhone's Calendar on the Exchange account, moved in Aperio. First set Settings → Logs to Debug: the GetItem answer ("EWS response") shows whether StartTimeZone carries Periods, that is, whether Exchange sent the zone's definition. Two outcomes are right:
    • with a definition, or a zone Aperio knows: Outlook shows the new day as one day;
    • with only an unknown zone name: Aperio says the day cannot be changed here (237), and nothing changes.
  7. A timed appointment made in Outlook, made all-day in Aperio. Outlook should show one day. (One made in Aperio is stored in UTC and shows nothing new.)
  8. A timed daily series split "this and all following" from its third day. Does the cut day show once or twice? Also check that a day deleted before the cut stays deleted, for PR 8.
  9. A timed one-off made in Aperio for Monday 2.11. at 00:30, then made weekly (no weekday chosen) in one save. Which weekday and time does Outlook show? This is decision 234. The zone that goes with it is known to move the wall clock (round 1, stages 9 and 12).

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.

  1. Daily all-day series, 4 times from Monday 2.11.: 2.11. to 5.11. Passed. Outlook labels the zoneless series "(UTC+00:00) Monrovia, Reykjavik", which is Greenwich (43b).
  2. Every 2 weeks on Monday, 3 times: 2.11. to 30.11. Passed.
  3. Timed, 00:30, weekly with no weekday chosen: Mondays 2.11. to 16.11., 00:30 to 01:30. Passed.
  4. Outlook all-day appointment moved to Tuesday: Tuesday, one day, in Aperio and Outlook. Passed (47a).
  5. Outlook all-day series: an exception moved (47a) and a repeat-only change (the first-day rule): as expected. Passed.
  6. Not possible as planned: Exchange is not in Toni's iPhone Calendar. Instead the program created two all-day items with a definition: a custom zone UTC+03:00 under the id "Customized Time Zone" (Exchange returned it so), and a zone named "Europe/Berlin". Exchange sends the definition in GetItem and SyncFolderItems; 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.
  7. A timed item in W. Europe, created through EWS the way Outlook stores it, made all-day in Aperio: Tuesday, one day, Berlin midnights. Passed.
  8. Timed daily series split from its third day: the cut day shows twice. The old series is cut with UNTIL one 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).
  9. A single made in Aperio, Monday 00:30, made weekly in one save: Sundays at 23:30. The update sent the rule (from the stored UTC zone's Sunday, 234) and then the zone, without the start or the end, and the new zone kept the stored wall clock 23:30. This is consistent with 234 but does not show the order: the rule named Sunday itself, and a zone that keeps the wall clock keeps its day, so either order gives Sundays at 23:30. main's writer sends the same request. The next PR writes the zone, then the start and the end even when unchanged, then the rule on the new zone's day, and measures whether that lands.

Also measured: a W. Europe series reads back with offset dates (2026-11-04+01:00) without a TimeZoneContext.

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_day and 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:

  • A flipped all-day flag. Toggling all-day without moving the start instant left a kept rule on the old first day. The two read the day on different clocks: the device's day, or the stored zone's. A written slot now takes the kept rule along whenever the first day moves.
  • The 1 January marker. Windows marks a year that opens on daylight time with a change at 1 January 00:00 (Moscow 2014, Brazil 2019). It was read as a clock change, so that day's midnight came out an hour late. It now opens the year.
  • An absurd bias. A bias beyond a day overflowed the clock arithmetic and panicked; it is now refused as BadBias.
  • Wording:
    • The German sentence has Toni's semicolon again (237); the test asserts it whole.
    • 234 and 233 are marked as decisions, not measurements. Live step 9 now looks at 234.
    • The guides no longer name the iPhone as a known-good case.
    • The fixtures are marked synthetic.
    • The 18 Europe/Berlin items are named as timed singles.
    • The 217 test has its doc comment back.
    • TODO says old series carry the weekday before the intended one, not always Sunday.
    • The old flag is described as a latent hazard, not a bug that happened.

Red proofs:

  • the flipped flag;
  • the 1 January marker;
  • the bias bound.

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:

  • A withheld day reported as "may have landed". Without the copy, the all-day write was withheld, but the read's own error (a network error) reached the user as "may have reached the server", and a split would not take its new part back. It is now refused as copy-unreadable, carried as Forbidden, with its own sentence.
  • Definitions cached for mapped ids. Exchange can send a definition with a zone the table maps. The parser kept it, so the cache grew for every Outlook item. A definition is now kept only for an id that names no known clock.
  • Live steps 5 and 7 could not show 47a. Step 5 changed an occurrence before the repeat; step 7 used an appointment made in Aperio, which is stored in UTC. Both now use Outlook items that show the change.
  • No test for a failed copy read. The adapter test now also answers GetItem with HTTP 503.
  • Stale generator comments. Round 3's steps said "as this build sends it" and "NOT Aperio's rule yet". They are now historical: what round 3's build sent, and the 47a prototype as Aperio's rule since 8a.

Red proofs:

  • copy-unreadable mapped to Network instead of Forbidden;
  • the definition kept for a mapped id.

Third check (02e46df)

Five findings were confirmed:

  • A zoned timed series updated without the copy still started on the UTC date. When the copy read failed, the rule's clock fell back to UTC, while the same request wrote the series' zone. Monday 00:30 in Berlin went out as StartDate Sunday. 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". Those keep their own error now: Authentication or NotFound, which the desktop counts as nothing written, as before this PR.
  • The round 3 generator claimed byte-for-byte history. R3-1 and R3-2 carry 8a's StartDate (2026-10-19 on a Berlin device; round 3 sent 2026-10-18), which the comments now say. R3-7b, not R3-7, is the measured 47a exception step.
  • Live step 6 could not show the definition at the default log level. It now says to switch the log to Debug and names both right outcomes.
  • The 234 test comment stated an inference as Exchange's behaviour. It now says it is a decision, not a measurement.

Red proofs:

  • the blind clock taken out (UTC again);
  • every read error wrapped as copy-unreadable again.

Fourth check (fbd2591)

Four findings were confirmed, two of them the same:

  • The log said a blind write went out when none did. When the copy could not be read, the adapter logged "writing every field not kept" before it knew whether the update would be sent. Now it logs the failed read, then what happened: the all-day day withheld (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.
  • Decision 233's wiring had no test. Every two-zone test built the zones by hand. An adapter test now reads a Greenwich start and a W. Europe end from the server copy and writes both, and a read test takes a Tokyo start and a Honolulu end through to_event and both override_event paths.
  • Two docs described the rule clock as before the third check: the event_to_update_field_xml_in comment and DESIGN-series-time-zone.md.

Red proofs, each failing its test:

  • the end zone taken from the start zone in StoredZones::of;
  • in to_event;
  • in an exception's own row;
  • in an inherited exception's row.

Fifth check (bea6fc0)

Three findings were confirmed, plus one a verifier set aside:

  • A blind save with nothing to send still logged a write. The "writing without the current copy" line now comes after the empty-update check, so it is logged only when an update goes out.
  • ews.md's Testing paragraph still called round 3's generator Aperio's current requests. It now says the files are historical since 8a and which ones are Aperio's rule.
  • A test comment named ews_all_day_boundary, which this PR replaced with all_day_boundary.
  • The German reasons had the wrong gender. They stand only in the split's sentences, about "die alte Serie": "Exchange speichert sie …, deshalb lässt sich ihr Tag …" and "Aperio konnte ihren aktuellen Stand …". A verifier called this a wording preference; it is a grammar error, so it is fixed, and the test asserts both sentences whole.

Sixth check (9b05c11)

Two findings were confirmed, both small:

  • A blind series update logged "cannot store this zone" twice. The third check's blind clock looked the written zone up a second time. The update builder now looks it up once and uses it for both the clock and the zone it writes, as the create does.
  • The round 3 generator's doc still opened with the old "NOT Aperio's rule" list, and ews.md left out that R3-1 and R3-2 carry 8a's StartDate. The list is now history, and ews.md names the one field that differs.

Seventh check (ca398e0)

One finding was confirmed. The reviewer also checked ZoneRules against 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_of and wall at every half hour, with no mismatch.

  • An id holding & never matched its target. The walker kept attribute ids as written (&amp;), 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 the StartTimeZone/EndTimeZone ids, 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

  • ews.md:
    • the first-day rule;
    • 47a on the stored zones;
    • zone definitions;
    • the refusals (day-zone-unreadable, copy-unreadable);
    • the all-day quirk now names the zones read;
    • the relabelling is called "before PR 8a".
  • DESIGN-series-time-zone.md:
    • 47a marked built;
    • stage 5 split into the Exchange part (done) and Microsoft 365 (open);
    • the "Ungeprüft" list gains what 8a leaves unmeasured.
  • TODO:
    • 47a and the date bug marked ↻;
    • flags for the old series (231), Microsoft 365, the zone-and-rule update and the doubled cut day (235), with the live results.
  • Troubleshooting guide, German and English: what the new sentences mean and what to do.

The phone needs a fresh .so and XCFramework.

🤖 Generated with Claude Code

Timtam and others added 19 commits October 10, 2026 02:51
… (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 '&amp;' 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>
@Timtam
Timtam merged commit 279f4e3 into main Oct 10, 2026
13 checks passed
@Timtam
Timtam deleted the feat/ews-writer-47a-date branch October 10, 2026 17:35
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.

1 participant