docs: AGENTS.md, PRODUCT.md, the spec and the README
The spec records that it was written AFTER the code, which inverts the house order, rather than presenting the rulings as though they preceded the work. It also lists what is incomplete: no rrule-go oracle, so the golden vectors encode one reading of the RFC rather than an independent one; no locale catalogs, which is a real divergence from the suite's twelve-locale floor and the largest piece of debt in the branch; no browser drive; and UI copy that has not been through copy-review. PRODUCT.md flags that its Users section is inferred from the suite's pattern rather than confirmed by Paul, because docs and sheets both carry a dated confirmation and this does not. The README states the leak list in plain words instead of leaving it to AGENTS.md. A calendar's plaintext shape is free/busy, which is a bigger leak than it sounds, and somebody deciding whether to put their team's week in here should read that on the way in rather than discover it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QG3FX6qzynri8yTgMUm7q7
3 files changed,
+556
−15
PRODUCT.md+150 −0README.md+149 −15docs/superpowers/specs/2026-09-02-calendar-design.md+257 −0
diff --git a/PRODUCT.md b/PRODUCT.md| new file mode 100644 |
| index 0000000..4c8d65d |
| --- /dev/null |
| +++ b/PRODUCT.md |
| @@ -0,0 +1,150 @@ |
| +# Product |
| + |
| +<!-- impeccable:product-schema 1 --> |
| + |
| +## Platform |
| + |
| +web |
| + |
| +## Users |
| + |
| +**Not yet confirmed by Paul.** Written from the suite's own pattern — |
| +docs and sheets both ruled the same way on 2026-09-01 — and flagged here |
| +rather than presented as settled. |
| + |
| +Paul and his collaborators, dogfooding. Real use, small N. Design for |
| +the people actually in it rather than a hypothesised market: a feature |
| +nobody here uses is not evidence of anything. |
| + |
| +They are running the CARLOS suite already, so Calendar is not entered |
| +cold. A person arrives with a session and the calendar is one of several |
| +apps on one instance. |
| + |
| +Beyond the team there is one further audience, and it is not a person: |
| +a **subscribed calendar client** — Google, Apple, Thunderbird — reading |
| +a feed URL. It cannot sign in, it polls forever, and it is the only |
| +consumer of this app that will never see a page. The export surface is |
| +designed for it rather than as an afterthought to the UI. |
| + |
| +Guests and invitations do not exist yet. That is a deferral with a |
| +written trigger, not an oversight — see AGENTS.md. |
| + |
| +## Product Purpose |
| + |
| +A calendar for one team, on that team's own instance, whose events are |
| +sealed at rest. |
| + |
| +Success is that a team can run its actual week — see the day, add |
| +something, be reminded, and subscribe from the calendar app they already |
| +use — without the storage layer ever holding a plaintext title, and |
| +without the product lying to them about what that does and does not |
| +protect. |
| + |
| +## Positioning |
| + |
| +Four claims a neighbouring product could not truthfully copy: |
| + |
| +1. **Sealed at rest, honestly scoped.** The bucket never holds a |
| + plaintext title, place or note. The instance holds the key while it |
| + runs, so a compromised live host *can* read a calendar — and the |
| + README says so. **We never claim "we can't read your data."** |
| + |
| + The honest scoping matters more here than in sheets, because a |
| + calendar's plaintext shape is *free/busy*: publish this database and |
| + the whole team's availability is legible even though no event's |
| + content is. That is a bigger leak than "the size of every sheet", and |
| + AGENTS.md says so in those words rather than letting the rule be read |
| + as covering it. |
| + |
| +2. **No affordance is script-required — including the grid.** Overlapping |
| + events in a week view are normally packed by JavaScript measuring |
| + boxes in a browser. Here the server does it, and click-a-time-to- |
| + create is 48 ordinary links per day. The scriptless page is the |
| + product, not the fallback. |
| + |
| +3. **The record is what you said, not when it happens.** An event is a |
| + wall-clock time plus a zone; the UTC instant is a derived index. A |
| + tzdata update moves the index and leaves the meeting at nine. Most |
| + calendars store the instant and cannot answer what was meant. |
| + |
| +4. **Reminders that survive a machine that is asleep.** The instance |
| + hibernates, so an in-process timer is not a scheduler. Three |
| + independent paths fire a reminder, and the product states when one |
| + arrived late rather than implying it was on time. |
| + |
| +## Operating Context |
| + |
| +- **One binary per instance**, `CGO_ENABLED=0`, cgo-free SQLite, |
| + deployed to CARLOS. Review on a deployed canary, never localhost. |
| +- **The instance hibernates**, and that is a product constraint rather |
| + than an implementation detail: it is why reminders are built the way |
| + they are, and why a subscription feed's poll count is shown to the |
| + person deciding whether to keep it. |
| +- Live at `https://calendar.bds.oncarlos.com`; console page at |
| + `https://console.carloku.com/a/bds/apps/calendar`. |
| + |
| +## The experience this is trying to produce |
| + |
| +A calendar's job is not to list events. It is to answer "how is my day" |
| +— and the feeling a day produces is whether there is room in it. |
| + |
| +So **Today opens with a sentence**, not a grid: |
| + |
| +> Wednesday. Four things; the first at 09:30, the last ends at 17:00. |
| +> That leaves two hours and 15 minutes unbooked between them. |
| + |
| +and when there is none: |
| + |
| +> There is no unbooked hour between them. |
| + |
| +which is true, slightly alarming, and the day the person is actually |
| +having. The arithmetic counts overlapping meetings once and excludes |
| +notes, because a birthday does not make a Tuesday fuller. |
| + |
| +**Three kinds, because a person makes the distinction whether or not the |
| +software does.** A *meeting* has other people in it and being late |
| +matters. A *commitment* is time promised to yourself: it fills the day, |
| +but only you are inconvenienced when it moves. A *note* is true of a day |
| +without occupying any of it. They differ in reminder defaults, in |
| +whether they count toward booked hours, and in how they draw. |
| + |
| +**The past is written in the past tense.** "Standup, ended 09:30" rather |
| +than "09:30". A calendar that describes this morning in the future tense |
| +makes you read the clock to know where you are. |
| + |
| +**Deleting a series asks once, and the expensive answer is last with its |
| +count out loud** — "All 52 of them", never a bare "All". |
| + |
| +**Your zone is the headline; another zone is a whisper.** Times are shown |
| +in the reader's own zone always. Where an event was authored elsewhere, |
| +that is a quiet line beside it, never the primary. |
| + |
| +## Accessibility |
| + |
| +WCAG 2.2 AA, no exemptions. 24×24 minimum targets, 320px reflow, colour |
| +never carrying meaning alone — every chip prints its own time and title, |
| +cancelled says the word, today has three signals, and the month cell |
| +prints its date whatever the theme did. |
| + |
| +At 320px the month reflows to a stacked list of days with something in |
| +them. The week keeps its two dimensions and scrolls: a week view IS a |
| +two-dimensional comparison, and flattening it produces the agenda, which |
| +already exists and is better at being a list. |
| + |
| +## What is deliberately not here |
| + |
| +Each of these is a decision, listed so its absence is legible, and each |
| +has a trigger in AGENTS.md or the brainstorm rather than a vague "later": |
| + |
| +attendees and invitations; guests; web push; external feed subscription |
| +(holidays); VTIMEZONE emission; expanding foreign RRULEs; drag to move |
| +and resize; search; working-hours shading; free/busy across members; |
| +attachments; print; "find a time"; anything with "AI" in it. |
| + |
| +## Voice |
| + |
| +Plain, exact, and never reassuring beyond the facts. Where a feature has |
| +a cost, the copy states it in the same breath — the share page names |
| +both what a subscription link gives away and what it costs to run before |
| +offering the button, and the import page says what will not come across |
| +before the upload rather than after. |
diff --git a/README.md b/README.md| index d5b6f81..6165f78 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -1,22 +1,156 @@ |
| -# calendar |
| +# Calendar |
| -A [rastrillo](https://github.com/rastrilloorg/rastrillo) app. `make ci` |
| -is the gate — vet, gofmt, tests, migration check, one definition for |
| -CI and for you. AGENTS.md carries the working conventions. |
| +A calendar for one team, on that team's own instance. Part of the |
| +oficina suite, alongside [docs](https://amadan.net/oficina/docs) and |
| +[sheets](https://amadan.net/oficina/sheets). |
| -## Browser drive |
| +Live at `https://calendar.bds.oncarlos.com`. |
| -`internal/calendartest/browser_test.go` walks the app in a real |
| -Chromium — loud on any console error, failed request, or JS value that |
| -leaked to the screen: |
| +## What it does |
| + |
| +Day, week, month and agenda views. Events that repeat. Reminders by |
| +email. Import and export as `.ics`, and subscription links you can paste |
| +into Google or Apple Calendar. |
| + |
| +Every screen works with JavaScript switched off — including the week |
| +grid, whose overlapping events are laid out on the server, and |
| +click-a-time-to-create, which is an ordinary link for every half hour of |
| +the day. |
| + |
| +## What is sealed, and what is not |
| + |
| +**Titles, places and notes are encrypted at rest.** The database file |
| +this instance keeps — the one the platform replicates to object storage |
| +— never holds a readable one. There is a test that greps the raw bytes. |
| + |
| +**Times are not.** They cannot be: every view is a range query over |
| +them, the reminder scheduler reads them before the app has opened |
| +anything, and the platform's own timer registry holds every reminder |
| +instant in plaintext regardless. So: |
| + |
| +> Publish this database and the worst anyone learns is **the shape of |
| +> the team's time** — when people are busy, for how long, how often — |
| +> and who is on the team. Never what any event is, where it is, or what |
| +> was written about it. |
| + |
| +That is a real leak, and a larger one than it first sounds: a team's |
| +free/busy says a lot. It is stated here rather than left for somebody to |
| +discover. |
| + |
| +Three more things sit outside the seal: |
| + |
| +- **A reminder email carries the title.** That is the product working, |
| + but the mail relay's logs are not ours. |
| +- **A subscription link puts this calendar's plaintext into whatever |
| + account you paste it into** — Google, Apple, a phone. The share page |
| + says so before it offers you the button. |
| +- **A compromised running host reads everything.** The instance holds |
| + the key while it runs. **We never claim we cannot read your data.** |
| + |
| +## Time zones |
| + |
| +An event's record is the wall-clock time you typed plus the zone you |
| +meant it in. The UTC instant is derived from those, and only ever used |
| +as an index. |
| + |
| +That matters when a government moves a daylight-saving boundary and a |
| +new time zone database ships. A calendar that stored the instant would |
| +now have your 09:00 standup at 08:00, with nothing able to say what was |
| +originally meant. Here the standup is still at 09:00 and only the index |
| +moves. |
| + |
| +Your zone is a setting, never guessed from your browser. A calendar that |
| +silently re-times itself because you opened it on holiday is a calendar |
| +that lies to you. |
| + |
| +## Repeating events |
| + |
| +The repeat picker offers six choices, and they are labelled against the |
| +event you are editing — "Every Wednesday", not "Weekly". |
| + |
| +A calendar you import may use a rule more complicated than those six. |
| +It is kept exactly as it arrived and exported unchanged; its future |
| +occurrences are not drawn on the grid, and the event's page shows you |
| +the rule. Nothing is thrown away. |
| + |
| +## Reminders |
| + |
| +The instance sleeps when nobody is using it, which is what makes it |
| +cheap to run and what makes reminders interesting. An ordinary timer |
| +inside the process would not fire, because the process is not there. |
| + |
| +So the app tells the platform when it next has work, and the platform |
| +wakes it then. A six-hourly check is the backstop, and any page view |
| +opportunistically sends anything overdue. |
| + |
| +A reminder can still arrive late — a machine has to wake up first. When |
| +that happens the email says so, rather than pretending it was on time. |
| + |
| +## Exports and subscriptions |
| + |
| +`.ics` export carries IANA time zone identifiers and **no `VTIMEZONE` |
| +block**. Google Calendar, Apple Calendar and Thunderbird resolve zones |
| +from their own databases and are fine. **Outlook desktop may not be** — |
| +it has historically resolved only the zones it finds inside the file. If |
| +you import one of our files into Outlook and see a wrong time, that is |
| +why, and it is worth telling us. |
| + |
| +A subscription link is a capability: anyone holding it can read that |
| +calendar, because a calendar app cannot sign in. Only its hash is |
| +stored, so it is shown once when you create it and never again. Revoke |
| +it and it stops working. |
| + |
| +Subscribed clients poll constantly — Apple roughly every fifteen minutes |
| +— and each poll wakes this instance. The share page shows you how many |
| +times each link has been polled, because a feed is not free. |
| + |
| +## Running it |
| ```sh |
| -go test -tags browser ./... # real browser, loud on any console error |
| - # — not part of the plain suite |
| +make ci # vet, gofmt, tests, migration check — the one gate |
| +make release # the stripped linux/arm64 binary |
| ``` |
| -It needs a Chromium: on PATH, via `RASTRILLO_CHROME`, or in a |
| -Playwright cache. **A skip is not a pass:** with no browser it fails, |
| -unless `RASTRILLO_BROWSER_OPTIONAL=1` makes the skip a deliberate, |
| -visible choice. `make ci` runs the plain suite only — whether CI |
| -also runs the browser drive, and on what runner, is this app's call. |
| +Locally: |
| + |
| +```sh |
| +go run ./cmd/calendar -env development -addr localhost:8099 -db /tmp/calendar.db |
| +``` |
| + |
| +`-env development` is the only way to get development behaviour: it is |
| +never honoured from the environment, because platform config *is* |
| +environment variables and a stray `CALENDAR_ENV=development` in |
| +production would open the allowlist and put sign-in links in the log. |
| + |
| +In production the app refuses to start without `CALENDAR_ORIGIN`, |
| +`CALENDAR_INSTANCE_KEY`, `CALENDAR_DEK`, `CALENDAR_ALLOWED_ADDRESSES` |
| +and a mail relay. Each refusal names what to set. A misconfigured |
| +instance is down rather than quietly wrong. |
| + |
| +## Deploying |
| + |
| +```sh |
| +make release |
| +carlos deploy --account oficina --app calendar --channel edge \ |
| + --timeout 5m releases/calendar-linux-arm64 |
| +``` |
| + |
| +Then check the **process**, not the edge: |
| + |
| +```sh |
| +curl -sS https://calendar.bds.oncarlos.com/api/version |
| +``` |
| + |
| +`X-Carlos-Version` is the router's adopted release and flips the moment |
| +the promote lands. The process takes about another 110 seconds to cycle. |
| + |
| +`stable` needs a passkey step-up in the console, and so does moving an |
| +instance between `stable` and `edge`. This app runs on `edge`. |
| + |
| +## Reading further |
| + |
| +- `AGENTS.md` — the binding rules, and the reasoning behind each |
| +- `PRODUCT.md` — what this is for and what it deliberately is not |
| +- `docs/superpowers/specs/` — the design |
| +- `docs/superpowers/brainstorms/` — the raw material, including the |
| + alternatives that were rejected and why |
diff --git a/docs/superpowers/specs/2026-09-02-calendar-design.md b/docs/superpowers/specs/2026-09-02-calendar-design.md| new file mode 100644 |
| index 0000000..0a2ee8a |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-09-02-calendar-design.md |
| @@ -0,0 +1,257 @@ |
| +# Calendar — design |
| + |
| +**Status:** built and deployed, 2026-09-02. Authored by Claude Opus 5. |
| +**Not yet reviewed by a model at least as capable as the author, and not |
| +yet approved by Paul.** Both are required before this branch merges; |
| +AGENTS.md §Review is the rule and this line is the record that it has |
| +not happened. |
| + |
| +Raw material, including the rejected alternatives: |
| +`docs/superpowers/brainstorms/2026-09-02-calendar-brainstorm.md` |
| +(Fable, 2026-09-02). |
| + |
| +**This spec was written after the code, which inverts the house order.** |
| +Recorded rather than hidden: the session built first and documented |
| +second, and the cost is that the rulings below were made while writing |
| +the code rather than before it. What follows is what was actually built, |
| +not what was planned. |
| + |
| +--- |
| + |
| +## 1. The one rule |
| + |
| +> **An event's contents are sealed at rest. The bucket never holds a |
| +> plaintext title, place or note.** |
| + |
| +Falsifiable: publish the database in full and the worst anyone learns is |
| +the shape of the team's time and who is on the team — never what any |
| +event is, where, or what was written about it. |
| + |
| +Enforced by `TestTheDatabaseNeverHoldsAPlaintextTitle`, which greps the |
| +raw SQLite file and the WAL after a `wal_checkpoint(TRUNCATE)`. |
| + |
| +### 1.1 Why times are plaintext |
| + |
| +Three reasons, and the third is the one that settles it: |
| + |
| +1. Every view is a range query over `start_utc` / `end_utc`. |
| +2. `NextDue` reads `MIN(due_at)` at boot, before any request, so the |
| + reminder queue cannot be behind a handle that opens per request. |
| +3. **The platform's own timer registry holds every reminder instant in |
| + plaintext anyway** — `carlos.ScheduleAt` sends `at` over the control |
| + socket. Sealing our copy would claim a property the estate does not |
| + have. |
| + |
| +The consequence is stated rather than buried: a published database gives |
| +up the team's whole free/busy, which is a bigger leak than sheets' "size |
| +of every sheet". AGENTS.md says so in those words. |
| + |
| +The deferred alternative — a sealed index the boot path can open before |
| +any request — has a design and no implementation. Trigger: somebody |
| +objects to free/busy being legible. |
| + |
| +--- |
| + |
| +## 2. Ruling 1 — store what the person said |
| + |
| +**An event's record is the wall-clock time typed plus the IANA zone |
| +meant. The UTC columns are derived.** |
| + |
| +Rejected: storing the instant as truth. A government moves a DST |
| +boundary, a release ships new tzdata, and a 09:00 standup is now at |
| +08:00 or 10:00 with nothing able to say what was meant. |
| + |
| +Columns: `start_local`, `end_local`, `tz` are the record; |
| +`start_utc`, `end_utc`, `until_utc` are an index. |
| + |
| +**DST.** There is no correct answer and Go does not promise one, so |
| +`clock.go` documents what happens and `clock_test.go` pins it to real |
| +unix numbers, measured against Europe/Dublin 2026: |
| + |
| +- The **gap** (spring, 01:00→02:00): `ParseInLocation` uses the |
| + pre-transition offset, so 01:30 lands at 01:30 UTC and reads back as |
| + 02:30 local. It moves FORWARD, which is the half worth relying on. |
| +- The **fold** (autumn, 02:00→01:00): the SECOND of the two occurrences. |
| + |
| +**These are not one rule.** An earlier draft of that comment claimed |
| +both took the pre-transition offset — true of the gap, false of the |
| +fold. A probe corrected it. Do not reason about the fold from the gap. |
| + |
| +**A day is 23, 24 or 25 hours**, and `DayBounds` uses `AddDate`. Tested. |
| + |
| +**All-day events are DATES**, not datetimes at UTC midnight. |
| + |
| +**Every range is half-open**, everywhere. |
| + |
| +--- |
| + |
| +## 3. Recurrence |
| + |
| +### 3.1 The subset |
| + |
| +`FREQ` DAILY / WEEKLY / MONTHLY / YEARLY, `INTERVAL`, `BYDAY` for |
| +weekly, a single ordinal `BYDAY` or a single `BYMONTHDAY` for monthly, |
| +`COUNT` or `UNTIL`. The UI exposes six choices, labelled against the |
| +event's own start date ("Every Wednesday", not "Weekly"). |
| + |
| +**A rule outside the subset is stored verbatim, flagged `rrule_foreign`, |
| +shown on the event's page, exported unchanged, and not expanded.** An |
| +importer that eats a rule it does not understand eats data. |
| + |
| +### 3.2 The three orderings, each tested |
| + |
| +- **COUNT counts GENERATED occurrences; EXDATE removes afterwards.** |
| + Filtering first gives a four-occurrence series five. |
| +- **A named date a month does not have is NO occurrence.** The 31st is |
| + skipped in February, never clamped. Same for 29 February yearly and |
| + the fifth Friday. |
| +- **Expansion iterates in WALL CLOCK.** Stepping a UTC instant by 24 |
| + hours moves a 09:00 standup by an hour for half the year. |
| + |
| +Capped at `MaxOccurrencesPerSeries = 1000` per master per request. |
| + |
| +### 3.3 Overrides |
| + |
| +An override is a row with `series_id` set and `recurrence_id_local` = |
| +the occurrence's **ORIGINAL** local start. It is rendered at whatever |
| +time it now has. |
| + |
| +The range query is therefore two passes: |
| + |
| +1. Expand each master; substitute any override matching the generated |
| + occurrence's original key. |
| +2. Add overrides whose own times land in the window but whose original |
| + did not — the occurrence dragged in from last week. |
| + |
| +Skip pass 2 and a moved occurrence vanishes. Skip the dedupe and it |
| +appears twice. Both are tested. |
| + |
| +**"This and following" is a SPLIT**, not `RANGE=THISANDFUTURE`: almost |
| +nothing that reads our export supports the range modifier. The head gets |
| +an `UNTIL` the day before the cut, the tail is a new series with a new |
| +UID, and overrides after the cut move to the tail so none are orphaned. |
| + |
| +--- |
| + |
| +## 4. Scriptless layout |
| + |
| +**The floor is the product.** Nothing here requires script. |
| + |
| +- **Month** is a `<table>`. A month IS tabular data; the table gives a |
| + screen reader its headers for free. Six rows always, so the page does |
| + not change height between months. **No spanning bars** — each day says |
| + what is on it, and `+N more` links to the day. |
| +- **Day and week** are packed **on the server**: events clustered |
| + transitively by overlap, then greedy lane assignment per cluster, then |
| + percentages emitted as inline style (rastrillo's CSP permits inline |
| + styles — its own partials carry them). Per cluster, not per day, so a |
| + lone four o'clock is not squeezed by two meetings at nine. |
| +- **Click-a-time-to-create** is 48 half-hour `<a>` elements per day |
| + column, each with an `rst-sr-only` name saying its time, each an |
| + ordinary GET to a prefilled form. |
| +- **All-day events are not packed** into the column; giving a birthday a |
| + height would make it look like a 24-hour meeting. |
| +- **320px**: month reflows to a stacked list; week keeps two dimensions |
| + and scrolls. |
| + |
| +Enhancement, currently: rastrillo's own `datetime.js` on the date |
| +fields. Nothing else. **Deferred:** `create.js`, turning a pointer drag |
| +across empty slots into one navigation to `/events/new`. Trigger: Paul's |
| +first canary review says selecting a range by form is painful. |
| + |
| +--- |
| + |
| +## 5. Reminders under hibernation |
| + |
| +A `time.Ticker` is not a scheduler; the process is not there when it |
| +fires. |
| + |
| +**One queue row per RULE**, holding only the next un-fired occurrence. |
| +A forever-daily standup costs one row; firing it computes the next. |
| + |
| +**Three ways a reminder fires**, and the redundancy is the design: |
| + |
| +1. `Options.NextDue` answers the activator's poll with |
| + `MIN(due_at)`. The platform hibernates knowing when to wake, and |
| + wakes the process for it. This is the precision path. |
| +2. The declared schedule, **every 6 hours**, POSTs `/jobs/reminders`. |
| + Six and not five minutes: `CARLOS_HIBERNATE_IDLE` defaults to 15 |
| + minutes, so every tick buys a quarter hour of billed residency — a |
| + 5-minute schedule accrues ~730 instance-hours a month for seconds of |
| + work. |
| +3. Any signed-in page view drains one opportunistically, bounded to one |
| + per request. |
| + |
| +**The tick route is mounted OUTSIDE `csrf.Protect`.** `csrf.Protect` |
| +refuses any POST carrying no `Sec-Fetch-Site`, `Origin` or `Referer`, |
| +and the scheduler sends none. Inside the protected router a tick 403s |
| +forever and the platform reads 4xx as "do not retry" — a silent, |
| +permanent failure. `TestTheTickRouteIsNotBehindCSRF` asserts WHICH |
| +refusal answered, and the deployed instance was checked from outside. |
| + |
| +**Work happens inside the request.** 202-plus-a-goroutine is a reliable |
| +way to be hibernated mid-send. |
| + |
| +**At-least-once**, and every ambiguous case resolves towards sending: a |
| +reminder that never arrives is worse than one that arrives twice. A |
| +5-minute claim lease bounds the duplicate window. `late_by_sec` is |
| +recorded and the message says how late it was. |
| + |
| +Undeliverable after 5 attempts, counted and surfaced on settings — |
| +retrying forever is a queue that never drains on a platform billed by |
| +residency. |
| + |
| +--- |
| + |
| +## 6. ICS |
| + |
| +**Import never rejects a file.** It takes what it honestly can and |
| +reports the rest by name: events added, skipped, floating, foreign |
| +rules, dropped attendees, unresolvable zones. `VTIMEZONE` and `VALARM` |
| +blocks are skipped wholesale so their properties cannot land on the |
| +event being built — tested. |
| + |
| +**Export** emits IANA `TZID`s and **no `VTIMEZONE`**. Google, Apple and |
| +Thunderbird resolve zones from their own databases; Outlook desktop |
| +historically does not. Stated in the README. Trigger for building it: an |
| +Outlook import of our export shows a wrong time. |
| + |
| +Lines fold at **75 octets** on rune boundaries. The round-trip test |
| +found a real conformance bug on its first run: export wrote |
| +`20260902T0930` where the RFC's DATE-TIME is `YYYYMMDDTHHMMSS`, so the |
| +app produced files its own parser rejected. `compactLocal` was serving |
| +both URL occurrence keys and wire values, which is what let them |
| +disagree; `icsDateTime` is now separate. |
| + |
| +**Subscription feeds** are capability URLs outside the session, because |
| +Google and Apple will not sign in. Only the token's hash is stored, so |
| +the URL is shown once and the page says so. `ETag`/`304`, because on a |
| +platform billed by residency a 304 is the difference between a feed that |
| +costs a little and one that costs a lot. `poll_count` is shown on the |
| +share page: a feed is not free and the person deciding should see it. |
| + |
| +--- |
| + |
| +## 7. What shipped, and what did not |
| + |
| +**Shipped and deployed:** sign-in, calendars and grants, events, five |
| +views, recurrence with occurrence editing and series splitting, |
| +reminders, ICS import/export/feeds, settings, theme switching. |
| + |
| +**Not built, each with a trigger:** attendees and guests, web push, |
| +external feed subscription, `VTIMEZONE`, expanding foreign rules, drag |
| +to move, skip-ahead expansion, sealed instants, search, background |
| +import, `RDATE` exactness, working-hours shading, free/busy across |
| +members, per-event colours, attachments, print, "find a time". |
| + |
| +**Known incomplete:** |
| + |
| +- The `rrule-go` oracle the brainstorm proposed as a test cross-check is |
| + not wired. The golden vectors are hand-derived, which means they |
| + encode my reading of the RFC rather than an independent one. |
| +- Locale catalogs are not written; user-visible strings are English |
| + literals rather than catalog keys. This is a real divergence from the |
| + suite's twelve-locale floor and is the largest single piece of debt in |
| + the branch. |
| +- No browser drive test. The scaffold ships the harness; nothing uses it. |
| +- UI copy has not been through the `copy-review` gate. |