Per-user and per-project calendars readable in Outlook — unlock the scopes the feed already models, and solve the mirroring problem #160

Open
opened 2026-07-29 11:17:43 +00:00 by mAi · 2 comments
Collaborator

m, 2026-07-29: "Es geht um die CalDav Kalender, die man dann mit Outlook syncen kann." and then, after the Microsoft-365 constraint was put to him:

"I dont have IT consent. So we should create some calendar instance that allows being read into outlook. I think i have that already with my work calendar in caldav. But we need that with individual calendars and these calendars need to be in sync with the project calendars etc. I think that is the real challenge, mirroring things in multiple calendars so people are free to see a project calendar inside outlook or their personal calendar."

The constraint that shapes everything

HLC is on Microsoft 365, and Microsoft 365 does not offer CalDAV. Verified two ways: our own docs/guide-outlook-calendar-subscription.md states it ("Microsoft 365 und Exchange Online bieten kein CalDAV an"), and both hlc.com and hoganlovells.com carry MS=ms… M365 domain-verification TXT records behind their Mimecast MX.

So:

  • Microsoft Graph / EWS is out. It would give real two-way sync natively, but it needs an app registration and HLC IT consent, and m has stated he does not have that.
  • A CalDAV server does not help Outlook. M365 Outlook cannot consume CalDAV without a third-party connector plugin, which IT may block.
  • The read-into-Outlook ICS subscription is the route that works today, with no IT involvement. That is the mechanism this issue builds on.

Read-only is therefore accepted, not overlooked. The ask is not two-way sync; it is more than one calendar, each subscribable.

Most of this is already built and was deliberately capped

This is v2 of an existing feature, not a new one. Measured against prod and the code:

what state
paliad.calendar_feeds.scope_kind CHECK all_visible, personal_only, project, client, litigation, patent, case (mig 192)
paliad.user_calendar_bindings.scope_kind CHECK the same seven (mig 101, CalDAV push)
scope_id constraint user-anchored scopes carry none; every other scope must — already enforced
include_personal, include_details already columns
scopeLabel() in settings-feeds.ts already renders personal_only and project
feeds in prod 1, scope_kind='all_visible'
user_calendar_bindings rows 0
user_caldav_config rows 0

And the cap is explicit in frontend/src/client/settings-feeds.ts:203:

scope_kind: "all_visible",
// m's Q1 answer scoped every feed to "the projects you are a team
// member of", so v1 offers no scope picker — one option is not a
// choice, and a picker implying otherwise would be a lie.

That was the right call for v1. m has now asked for the choice, so the picker stops being a lie.

The real challenge, in m's words: mirroring

One appointment can legitimately belong in several calendars at once — a project calendar and the individual's own. If a user subscribes to both, the same event must not appear twice in Outlook.

This is the part that is genuinely unbuilt and it is the reason this issue is not just "add a dropdown":

  • UID stability across feeds. An event's UID must be the same in every feed it appears in, so a client that holds two subscriptions can recognise it as one event. Get this wrong and every lawyer subscribed to two calendars sees doubles.
  • UID stability across time. The UID must not change when the event is edited, or Outlook will accumulate ghosts rather than update in place.
  • What a project feed shows that a personal feed does not, and vice versa — is a deadline assigned to me on project X in both? Duplication is only a problem if the answer is yes, and the answer should be a deliberate decision rather than a side effect.
  • include_personal interacts with this. It already exists; its meaning under multiple scoped feeds needs restating.

Scope

  1. Expose the scope picker in the feed-create flow. The storage, constraints and label rendering already exist; wire the choice.
  2. Make the feed generator honour scope_kind / scope_id. Verify what it does today — it may already, since only all_visible has ever been requested.
  3. Solve the mirroring/dedup problem and write down the rule. This is the deliverable, not the picker.
  4. Update docs/guide-outlook-calendar-subscription.md — it currently describes one feed. Its Part 0 decision aid becomes more important, not less, once there are several.

Out of scope

  • Two-way sync. Not possible without IT consent; do not design toward it.
  • A CalDAV server in paliad. It would not help M365 Outlook and nobody has asked for the clients it would help.
  • Personal (non-matter) appointments. appointments.project_id is nullable and 0 rows use it — a real gap, but a different one. m was asked and did not choose it.

Verified facts to carry in

  • Refresh cadence is Outlook's, not ours — often 3 to 24 hours on Classic. A user reporting a stale calendar is usually seeing that, not a bug.
  • A sparse calendar is correct behaviour. Measured example: one user with 1 team membership has exactly 1 appointment and 1 deadline.
  • CALDAV_ENCRYPTION_KEY seals calendar_feeds.token_encrypted so a subscription link stays re-displayable. Unset ⇒ no new feed can be minted (501), though existing feeds still list and revoke.
  • PALIAD_BASE_URL is baked into every minted subscription URL. Wrong value ⇒ every link minted in that environment is wrong, and Outlook has no edit-URL affordance — users must remove and re-add the calendar.
m, 2026-07-29: *"Es geht um die CalDav Kalender, die man dann mit Outlook syncen kann."* and then, after the Microsoft-365 constraint was put to him: > "I dont have IT consent. So we should create some calendar instance that allows being read into outlook. I think i have that already with my work calendar in caldav. But we need that with individual calendars and these calendars need to be in sync with the project calendars etc. **I think that is the real challenge, mirroring things in multiple calendars so people are free to see a project calendar inside outlook or their personal calendar.**" ## The constraint that shapes everything **HLC is on Microsoft 365, and Microsoft 365 does not offer CalDAV.** Verified two ways: our own `docs/guide-outlook-calendar-subscription.md` states it ("Microsoft 365 und Exchange Online bieten kein CalDAV an"), and both `hlc.com` and `hoganlovells.com` carry `MS=ms…` M365 domain-verification TXT records behind their Mimecast MX. So: - **Microsoft Graph / EWS is out.** It would give real two-way sync natively, but it needs an app registration and HLC IT consent, and m has stated he does not have that. - **A CalDAV *server* does not help Outlook.** M365 Outlook cannot consume CalDAV without a third-party connector plugin, which IT may block. - **The read-into-Outlook ICS subscription is the route that works today, with no IT involvement.** That is the mechanism this issue builds on. Read-only is therefore accepted, not overlooked. The ask is not two-way sync; it is *more than one calendar*, each subscribable. ## Most of this is already built and was deliberately capped This is v2 of an existing feature, not a new one. Measured against prod and the code: | what | state | |---|---| | `paliad.calendar_feeds.scope_kind` CHECK | `all_visible`, `personal_only`, `project`, `client`, `litigation`, `patent`, `case` (mig 192) | | `paliad.user_calendar_bindings.scope_kind` CHECK | the same seven (mig 101, CalDAV push) | | `scope_id` constraint | user-anchored scopes carry none; every other scope must — already enforced | | `include_personal`, `include_details` | already columns | | `scopeLabel()` in `settings-feeds.ts` | already renders `personal_only` and `project` | | feeds in prod | **1**, `scope_kind='all_visible'` | | `user_calendar_bindings` rows | **0** | | `user_caldav_config` rows | **0** | And the cap is explicit in `frontend/src/client/settings-feeds.ts:203`: ```ts scope_kind: "all_visible", // m's Q1 answer scoped every feed to "the projects you are a team // member of", so v1 offers no scope picker — one option is not a // choice, and a picker implying otherwise would be a lie. ``` That was the right call for v1. m has now asked for the choice, so the picker stops being a lie. ## The real challenge, in m's words: mirroring One appointment can legitimately belong in several calendars at once — a project calendar and the individual's own. If a user subscribes to both, **the same event must not appear twice in Outlook.** This is the part that is genuinely unbuilt and it is the reason this issue is not just "add a dropdown": - **UID stability across feeds.** An event's `UID` must be the same in every feed it appears in, so a client that holds two subscriptions can recognise it as one event. Get this wrong and every lawyer subscribed to two calendars sees doubles. - **UID stability across time.** The UID must not change when the event is edited, or Outlook will accumulate ghosts rather than update in place. - **What a project feed shows that a personal feed does not**, and vice versa — is a deadline assigned to me on project X in *both*? Duplication is only a problem if the answer is yes, and the answer should be a deliberate decision rather than a side effect. - **`include_personal` interacts with this.** It already exists; its meaning under multiple scoped feeds needs restating. ## Scope 1. **Expose the scope picker** in the feed-create flow. The storage, constraints and label rendering already exist; wire the choice. 2. **Make the feed generator honour `scope_kind` / `scope_id`.** Verify what it does today — it may already, since only `all_visible` has ever been requested. 3. **Solve the mirroring/dedup problem** and write down the rule. This is the deliverable, not the picker. 4. **Update `docs/guide-outlook-calendar-subscription.md`** — it currently describes one feed. Its Part 0 decision aid becomes more important, not less, once there are several. ## Out of scope - Two-way sync. Not possible without IT consent; do not design toward it. - A CalDAV server in paliad. It would not help M365 Outlook and nobody has asked for the clients it *would* help. - Personal (non-matter) appointments. `appointments.project_id` is nullable and **0 rows use it** — a real gap, but a different one. m was asked and did not choose it. ## Verified facts to carry in - Refresh cadence is Outlook's, not ours — often 3 to 24 hours on Classic. A user reporting a stale calendar is usually seeing that, not a bug. - A sparse calendar is correct behaviour. Measured example: one user with 1 team membership has exactly 1 appointment and 1 deadline. - `CALDAV_ENCRYPTION_KEY` seals `calendar_feeds.token_encrypted` so a subscription link stays re-displayable. Unset ⇒ no new feed can be minted (501), though existing feeds still list and revoke. - `PALIAD_BASE_URL` is baked into every minted subscription URL. Wrong value ⇒ every link minted in that environment is wrong, and **Outlook has no edit-URL affordance** — users must remove and re-add the calendar.
Author
Collaborator

Design committed — shift-1 (cronus)

Doc: docs/design-calendar-scopes-mirroring-2026-07-29.md
Commit: https://mgit.msbls.de/m/paliad/commit/32a1a7723 (branch mai/cronus/m-paliad-160-per-user)

Design only, no code. Two of this issue's premises did not survive measurement, so they are corrected in the doc rather than built on.

Correction 1 — per-project feeds already ship

The matter page has „Diesen Kalender abonnieren" and it mints scope_kind:"project" today: frontend/src/client/projects-detail.ts:3069 (wireCalendarSubscribe), markup frontend/src/projects-detail.tsx:682-692, wired at :2302. The v1 cap at settings-feeds.ts:203 is a cap on the Settings create-flow only, not on the product. The generator honours the scope as well — AppointmentService.ForScope (appointment_service.go:806) and DeadlineService.ForScope (deadline_service.go:1212) serve all_visible / personal_only / project and refuse the four hierarchy scopes at mint with 400.

Correction 2 — stable UIDs do not make two Outlook subscriptions merge

The issue reads the UID as the dedup mechanism across feeds. It is not. A UID makes a client update an event in place when it refreshes one calendar. Each internet-calendar subscription in Outlook is its own calendar folder; the overlay view draws both folders on one grid. Microsoft documents no cross-folder deduplication, and none was found. Apple and Google Calendar behave the same.

So doubling is not solved by UIDs at all. It is decided by what a user's set of feeds is allowed to contain.

Not verified and not verifiable from here: no Outlook client on mRiver, no M365 account — the same limit the user guide records in its §3.4. The design is built so the answer does not change it, only how loudly we warn.

The six decisions

  1. UID = f(row id), nothing else. Already true (caldav_ical.go:34-44). Pinned as a contract, because the natural way to implement per-scope feeds badly is to put the project id in the UID — and a shipped UID cannot be changed without every subscriber seeing every event duplicated once.
  2. UID survives an edit. Verified, not assumed: every deadline write path is an UPDATE (deadline_service.go:592/697/749, projection_service.go:1873, scenario_builder_service.go:1148, rule_editor_orphans.go:227, dual_write.go:40); the only DELETE outside tests is the explicit user delete. No SEQUENCE — Paliad has no monotonic counter and a value that goes backwards is worse than an absent one.
  3. Overlap allowed, disclosed, never prevented. Paliad does not know which links a user pastes into which client. Two links may be one Outlook (doubles) or a laptop and a phone (the point). Refusing to mint would guess. Overlap is computable from (scope_kind, scope_id, include_personal) alone, so it can be shown at mint time and in the Settings list. Subtractive feeds were rejected: revoking a narrow feed would silently widen the wide one.
  4. include_personal is a union flag on project-anchored feeds only; at most one of a user's feeds should carry personal appointments. Default false on project feeds, moved from the call site to the service. No UI — prod has 0 personal appointments.
  5. personal_only stays out of the picker. It would mint a guaranteed-empty calendar (0 personal appointments; deadlines.project_id is NOT NULL, so a personal deadline cannot exist). Same class of lie the v1 comment refused, in the other direction.
  6. „Individual calendar" already exists and is all_visible — it is computed per user from team memberships on every poll. What does not exist is „meine Fristen": paliad.deadlines and paliad.appointments carry no assignee, owner, responsible or attendee field, only created_by, which records who typed the row in. A PA entering the partner's deadlines makes created_by wrong in the common case. That is a schema gap, not a feed scope.

What „unlock the picker" then means

Settings stops pretending every feed is all_visible: it shows the real scope, names the matter (scope_id is already in the JSON payload — models.go:565 — the TS type just drops it), and discloses overlap. The minting choice stays on the matter page, where the user has the matter in front of them.

Slices

S Work
S1 Pin the mirroring contract in tests (UID identical across scopes, unchanged across an edit)
S2 Settings shows the real scope and names the matter
S3 Overlap disclosure at mint and in the list
S4 Hierarchy scopes (client/litigation/patent/case) — only if m wants them
S5 Update docs/guide-outlook-calendar-subscription.md

Open for m (via head)

  1. Full scope picker in Settings, or matter-page minting + Settings as inventory?
  2. Hierarchy scopes now, or stay refused until asked?
  3. Confirm the overlap policy (allowed + disclosed).
  4. Is „meine Fristen" what „individual calendars" meant? If yes it is a schema change and its own issue.

Scale measured on prod 2026-07-29: 48 users, 19 projects, 11 deadlines, 2 appointments, 1 feed (all_visible), 0 CalDAV bindings.

## Design committed — shift-1 (cronus) **Doc:** `docs/design-calendar-scopes-mirroring-2026-07-29.md` **Commit:** https://mgit.msbls.de/m/paliad/commit/32a1a7723 (branch `mai/cronus/m-paliad-160-per-user`) Design only, no code. Two of this issue's premises did not survive measurement, so they are corrected in the doc rather than built on. ### Correction 1 — per-project feeds already ship The matter page has *„Diesen Kalender abonnieren"* and it mints `scope_kind:"project"` today: `frontend/src/client/projects-detail.ts:3069` (`wireCalendarSubscribe`), markup `frontend/src/projects-detail.tsx:682-692`, wired at `:2302`. The v1 cap at `settings-feeds.ts:203` is a cap on the **Settings** create-flow only, not on the product. The generator honours the scope as well — `AppointmentService.ForScope` (`appointment_service.go:806`) and `DeadlineService.ForScope` (`deadline_service.go:1212`) serve `all_visible` / `personal_only` / `project` and refuse the four hierarchy scopes at mint with 400. ### Correction 2 — stable UIDs do not make two Outlook subscriptions merge The issue reads the UID as the dedup mechanism across feeds. It is not. A UID makes a client update an event in place *when it refreshes one calendar*. Each internet-calendar subscription in Outlook is its **own calendar folder**; the overlay view draws both folders on one grid. Microsoft documents no cross-folder deduplication, and none was found. Apple and Google Calendar behave the same. So doubling is not solved by UIDs at all. It is decided by what a user's set of feeds is allowed to contain. Not verified and not verifiable from here: no Outlook client on mRiver, no M365 account — the same limit the user guide records in its §3.4. The design is built so the answer does not change it, only how loudly we warn. ### The six decisions 1. **UID = f(row id), nothing else.** Already true (`caldav_ical.go:34-44`). Pinned as a contract, because the natural way to implement per-scope feeds badly is to put the project id in the UID — and a shipped UID cannot be changed without every subscriber seeing every event duplicated once. 2. **UID survives an edit.** Verified, not assumed: every deadline write path is an `UPDATE` (`deadline_service.go:592/697/749`, `projection_service.go:1873`, `scenario_builder_service.go:1148`, `rule_editor_orphans.go:227`, `dual_write.go:40`); the only `DELETE` outside tests is the explicit user delete. No `SEQUENCE` — Paliad has no monotonic counter and a value that goes backwards is worse than an absent one. 3. **Overlap allowed, disclosed, never prevented.** Paliad does not know which links a user pastes into which client. Two links may be one Outlook (doubles) or a laptop and a phone (the point). Refusing to mint would guess. Overlap is computable from `(scope_kind, scope_id, include_personal)` alone, so it can be shown at mint time and in the Settings list. Subtractive feeds were rejected: revoking a narrow feed would silently widen the wide one. 4. **`include_personal`** is a union flag on project-anchored feeds only; at most one of a user's feeds should carry personal appointments. Default `false` on project feeds, moved from the call site to the service. No UI — prod has **0** personal appointments. 5. **`personal_only` stays out of the picker.** It would mint a guaranteed-empty calendar (0 personal appointments; `deadlines.project_id` is NOT NULL, so a personal deadline cannot exist). Same class of lie the v1 comment refused, in the other direction. 6. **„Individual calendar" already exists and is `all_visible`** — it is computed per user from team memberships on every poll. What does not exist is *„meine Fristen"*: `paliad.deadlines` and `paliad.appointments` carry **no assignee, owner, responsible or attendee** field, only `created_by`, which records who typed the row in. A PA entering the partner's deadlines makes `created_by` wrong in the common case. That is a schema gap, not a feed scope. ### What „unlock the picker" then means Settings stops pretending every feed is `all_visible`: it shows the real scope, names the matter (`scope_id` is already in the JSON payload — `models.go:565` — the TS type just drops it), and discloses overlap. The minting choice stays on the matter page, where the user has the matter in front of them. ### Slices | S | Work | |---|---| | S1 | Pin the mirroring contract in tests (UID identical across scopes, unchanged across an edit) | | S2 | Settings shows the real scope and names the matter | | S3 | Overlap disclosure at mint and in the list | | S4 | Hierarchy scopes (`client`/`litigation`/`patent`/`case`) — only if m wants them | | S5 | Update `docs/guide-outlook-calendar-subscription.md` | ### Open for m (via head) 1. Full scope picker in Settings, or matter-page minting + Settings as inventory? 2. Hierarchy scopes now, or stay refused until asked? 3. Confirm the overlap policy (allowed + disclosed). 4. Is *„meine Fristen"* what „individual calendars" meant? If yes it is a schema change and its own issue. Scale measured on prod 2026-07-29: 48 users, 19 projects, 11 deadlines, 2 appointments, 1 feed (`all_visible`), 0 CalDAV bindings.
Author
Collaborator

Built — shift-2 (cronus)

Branch mai/cronus/m-paliad-160-per-user, three commits plus a merge of main.

c4387a7 the four hierarchy scopes are served
0f1e2fc Settings inventories every subscription; overlap disclosed
7b8990a the UID contract pinned; guide + design doc updated

Q2 — hierarchy scopes

client / litigation / patent / case now cover the anchor matter and its whole subtree. One shared rule, scopeAnchorPredicate (internal/services/calendar_scope.go): exact match for project, the materialised-path descendant test for the four. Both ForScope implementations read it, so CalDAV and the ICS feed cannot drift.

The visibility predicate stays on the event's own project rather than the anchor. Visibility inherits down the tree so the two agree today; only the per-event form survives that ceasing to be true, and it costs the same JOIN.

Mint asserts the anchor's level: a client-kind feed on a case project is a 400. The scope is immutable once minted and scope_kind is what names the feed in the UI, so a mismatch would be permanent and the only remedy is remove-and-re-add in Outlook.

Two duplicate scope validators (validateScope in binding land, validateCalendarScope in feed land — same rule, different wording) are now one ValidateCalendarScope. The two tables share the CHECK it mirrors.

Q1 — both

Minting stays on the matter page. The subtree choice appears on client / litigation / patent, not on a case: a case is the bottom of the hierarchy, so both options would return the same calendar. It is not gated on "has children today" — the scope is immutable, so a litigation with no patents yet is exactly where "and everything below" is the right pick.

Settings is now the inventory. ListForUser LEFT JOINs the anchor matter for its title. The visibility predicate sits in the JOIN, not the WHERE: a feed whose matter the owner has since lost must still be listed — it is their link and they have to be able to revoke it — but it must stop naming a matter they can no longer see. In the WHERE it would hide the row and strand a live subscription.

Q3 — overlap

internal/services/calendar_overlap.go, a pure function of the scopes: all_visible meets everything; personal_only meets a matter feed only through include_personal; two anchored feeds meet when one's covered set contains the other's anchor. Shown in the Settings list and alerted at mint time, because minting is when the user is deciding where to paste the link. Never prevented.

The mirroring contract, pinned

internal/services/caldav_ical_uid_contract_test.go. The UID is a function of the row id and type and of nothing else — asserted across every ICSOptions shape the formatter accepts, across an edit changing title, date, updated_at and description at once, and against the literal spelling. Negative controls: the two documents in the edit case must differ (else the equality proves nothing), two deadlines must not share a UID, a deadline and an appointment carrying the same row uuid must not collide, and the feed must name an appointment identically to the CalDAV push.

Tests

The old hierarchy test asserted only that the four kinds were refused, so their SQL had never executed. Replaced with row counts over a seeded Client→Litigation→Patent→Case chain (4 / 3 / 2 / 1), a narrowness control against project scope on the same anchor, and a visibility control proving a stranger sees zero through a client-scoped feed. The overlap rule is asserted in both directions per case — it is documented as symmetric, and a one-directional prefix test is the obvious way to break that silently — plus a separator-aware control proving c.l does not swallow c.long.

Gate

Full scripts/ci-test-gate.sh against a private harness: go vet clean, whole suite with -race, 0 known-failing tolerated, no new failures. check-gofmt.sh exit 0. 420 frontend tests. Branch merged with main (which had moved) and re-run after the merge.

Not built, deliberately

The assignee column from m's Q4 second clause. Its own issue, per the head's instruction. No feed is scoped to "my deadlines" here.

One thing nobody here can check

That two overlapping subscriptions really do show doubles in a live Outlook. No Outlook client on mRiver and no M365 account — the same limit the user guide records in its §3.4, where this is now written down as point 6. Microsoft documents no cross-folder merge and none was found; the absence is the evidence. The design does not depend on the answer — it only changes how loudly we warn.

## Built — shift-2 (cronus) Branch `mai/cronus/m-paliad-160-per-user`, three commits plus a merge of `main`. | | | |---|---| | [`c4387a7`](https://mgit.msbls.de/m/paliad/commit/c4387a7) | the four hierarchy scopes are served | | [`0f1e2fc`](https://mgit.msbls.de/m/paliad/commit/0f1e2fc) | Settings inventories every subscription; overlap disclosed | | [`7b8990a`](https://mgit.msbls.de/m/paliad/commit/7b8990a) | the UID contract pinned; guide + design doc updated | ### Q2 — hierarchy scopes `client` / `litigation` / `patent` / `case` now cover the anchor matter **and its whole subtree**. One shared rule, `scopeAnchorPredicate` (`internal/services/calendar_scope.go`): exact match for `project`, the materialised-path descendant test for the four. Both `ForScope` implementations read it, so CalDAV and the ICS feed cannot drift. The visibility predicate stays on the **event's own** project rather than the anchor. Visibility inherits down the tree so the two agree today; only the per-event form survives that ceasing to be true, and it costs the same JOIN. Mint asserts the anchor's **level**: a `client`-kind feed on a case project is a 400. The scope is immutable once minted and `scope_kind` is what names the feed in the UI, so a mismatch would be permanent and the only remedy is remove-and-re-add in Outlook. Two duplicate scope validators (`validateScope` in binding land, `validateCalendarScope` in feed land — same rule, different wording) are now one `ValidateCalendarScope`. The two tables share the CHECK it mirrors. ### Q1 — both Minting stays on the matter page. The subtree choice appears on **client / litigation / patent**, not on a case: a case is the bottom of the hierarchy, so both options would return the same calendar. It is *not* gated on "has children today" — the scope is immutable, so a litigation with no patents yet is exactly where "and everything below" is the right pick. Settings is now the inventory. `ListForUser` LEFT JOINs the anchor matter for its title. **The visibility predicate sits in the JOIN, not the WHERE**: a feed whose matter the owner has since lost must still be listed — it is their link and they have to be able to revoke it — but it must stop naming a matter they can no longer see. In the WHERE it would hide the row and strand a live subscription. ### Q3 — overlap `internal/services/calendar_overlap.go`, a pure function of the scopes: `all_visible` meets everything; `personal_only` meets a matter feed only through `include_personal`; two anchored feeds meet when one's covered set contains the other's anchor. Shown in the Settings list and alerted at mint time, because minting is when the user is deciding where to paste the link. Never prevented. ### The mirroring contract, pinned `internal/services/caldav_ical_uid_contract_test.go`. The UID is a function of the row id and type and of nothing else — asserted across every `ICSOptions` shape the formatter accepts, across an edit changing title, date, `updated_at` and description at once, and against the literal spelling. Negative controls: the two documents in the edit case must differ (else the equality proves nothing), two deadlines must not share a UID, a deadline and an appointment carrying the same row uuid must not collide, and the feed must name an appointment identically to the CalDAV push. ### Tests The old hierarchy test asserted only that the four kinds were **refused**, so their SQL had never executed. Replaced with row counts over a seeded Client→Litigation→Patent→Case chain (4 / 3 / 2 / 1), a narrowness control against `project` scope on the same anchor, and a visibility control proving a stranger sees zero through a client-scoped feed. The overlap rule is asserted in both directions per case — it is documented as symmetric, and a one-directional prefix test is the obvious way to break that silently — plus a separator-aware control proving `c.l` does not swallow `c.long`. ### Gate Full `scripts/ci-test-gate.sh` against a private harness: `go vet` clean, whole suite with `-race`, **0 known-failing tolerated, no new failures**. `check-gofmt.sh` exit 0. 420 frontend tests. Branch merged with `main` (which had moved) and re-run after the merge. ### Not built, deliberately The assignee column from m's Q4 second clause. Its own issue, per the head's instruction. No feed is scoped to "my deadlines" here. ### One thing nobody here can check That two overlapping subscriptions really do show doubles in a live Outlook. No Outlook client on mRiver and no M365 account — the same limit the user guide records in its §3.4, where this is now written down as point 6. Microsoft documents no cross-folder merge and none was found; the absence is the evidence. The design does not depend on the answer — it only changes how loudly we warn.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#160
No description provided.