# Besluitenlog 2026-07-26 — panel-review op TEN-89 en TEN-122/TEN-59

> **Status:** ✅ Live (gebouwd, bereikbaar op genoemde route) · 🟡 Gedeeltelijk (werkt, met benoemd gat) · 🔧 In uitvoering · 📋 Gepland (met fase) · 💤 Bewust uitgesteld / non-goal

Twee wijzigingen van 25-07 verschoven bewust een veiligheidsgrens en zijn daarom
na de merge adversarieel gereviewd door een panel van drie onafhankelijke lenzen
(codex `gpt-5.6-sol`, GLM, en een Opus-subagent). De brief was voor alle drie
gelijk: *weerleg de correctheid*, met verplicht bestand:regel en een concreet
faalscenario, en markeer onbewijsbare claims als PLAUSIBLE.

- **TEN-89** — commit `64ed4ac4` + migratie `0077_ten89_replan_precondition.sql`:
  de preconditie van de `replan_recurrence`-RPC versoepeld van "is deze rij *nu*
  herhalend?" naar "is er *iets* om te herplannen?".
- **TEN-122 / TEN-59** — commit `57f8338c`: de TEN-1-invariant "toewijzing
  impliceert delen" gedegradeerd van een harde invariant naar een **default** met
  een **override**, geïmplementeerd zonder nieuwe kolom.

## Verdict

Het panel vond **drie reële defecten** en **één landmijn**. Geen ervan is
gevonden door alle drie de lenzen; de weging hieronder volgt de consolidatieregel
uit de `panel-review`-skill (≥2 lenzen = hoog vertrouwen), met één uitzondering:
een bevinding die met een runtime-probe is **bewezen** weegt zwaarder dan
consensus.

### Bevestigd door twee lenzen

**B1 — Een gedeeld lid dat "voor wie" wijzigde, draaide stil de héle bewerking
terug.** (codex, Opus.) De betrokkenen-default leidde uit een `member_ids`-
wijziging ook een wijziging van de deel-kolommen af. `guard_row_sharing`
(migratie `0044`) weigert die met 42501 wanneer de caller niet de eigenaar of een
beheerder is; de connector markeert zo'n fout als permanent en dropt de op —
waarna titel, tijd én betrokkenen bij de volgende checkpoint terugdraaien. Voor
`calendar_events` geruisloos, want die tabel zit niet in de drop-melder. Nieuw
door TEN-122. De doc-comment bij `canEditShareSettings` schreef deze gate al
voor; het agendaformulier hield zich er niet aan.

**B2 — Legacy agenda-rijen genezen niet zelf.** (codex, GLM.) Een afspraak van
vóór TEN-122 die "voor" een ander lid is gemaakt, staat op `visibility = 1` met
dat lid in `member_ids`. De override-derivatie leest dat permanent als een
bewuste uitsluiting, óók bij een gewone bewerking. Voor `tasks` had migratie
`0046` de historie rechtgezet; voor `calendar_events` was dat niet gebeurd.

### Bewezen door één lens

**B3 — Privé plus een gedeeltelijke override liet een nieuw betrokken lid stil
buiten de deling.** (Opus, aangetoond met een tijdelijke probe-test en daarna
zelf gereproduceerd.) Item voor B, gebruiker kiest bewust "Privé" (een geldige
override op B), zet daarna C bij "voor wie" zonder de deel-picker aan te raken.
De picker toont "Gedeeld met… C" aangevinkt, de opslag schrijft
`visibility = 1, shared_with_member_ids = null` — **C ziet niets**. Oorzaak: de
override werd als één scalaire boolean doorgegeven; die zette de fold uit, maar
niemand schaalde de zichtbaarheid mee, en de encoder gooit de deellijst weg bij
elke stand behalve "gedeeld met".

**B4 (landmijn) — Migratie `0046` was niet langer veilig om opnieuw te draaien.**
(Opus.) Haar predicaat — `visibility = 1` én toegewezen aan een niet-owner — is
sinds TEN-122 *exact* de signatuur van een bewuste override, terwijl de header
"idempotent" claimde en daarmee uitnodigde tot her-uitvoeren. Eén her-run zou
alle privé-overrides in één UPDATE hebben gewist. Zelf onafhankelijk nagerekend
en bevestigd voordat er iets aan is gedaan.

**B5 — De nieuwe RPC-preconditie is op de taak-caller gekozen; de klus-caller kan
hem niet dragen.** (Opus.) Een eenmalige klus naar wekelijks zetten slaat niets
op en meldt "Geen rechten om te herplannen": `guard_chore_task_core` (`0052`)
weigert élke client-anker-write op een `chores.task`, dus de RPC is de enige weg,
maar `0077` weigert een rij zonder reeks en zonder tellers. Pre-existent onder
`0055`, maar `0077` koos bewust een vorm die het gat openlaat.

### Verworpen of afgewaardeerd

- **RPC-atomiciteit** (codex: twee P1's) is door GLM en Opus gewogen als een
  **pre-bestaande last-write-wins-klasse** die TEN-89 breder bereikbaar maakt maar
  niet introduceert. Afgewaardeerd naar P2; niet in deze ronde gerepareerd.
- **"Niemand gekozen = hele gezin"** (codex P1) is een reële inconsistentie
  tussen label en opslag — het taakmodel, de agendatabel én het UI-label zeggen
  alle drie "hele gezin", de opslag maakt het privé — maar is **pre-existent** en
  door TEN-122 alleen in een test gecodificeerd. Dit is een **productvraag voor de
  eigenaar**, geen bug: betekent "voor wie" verantwoordelijkheid of
  zichtbaarheid? Zie `docs/beslissingen/index.html`.
- **Weggevallen testdekking**: Opus vond er géén. Elke omgekeerde assertie in de
  herschreven contracttests heeft motivering en vervanging; de gaten die het
  panel vond zijn nooit-gedekte gevallen, geen weggehaalde garanties.

## Wat er is gerepareerd

| # | Fix | Commit | Status |
|---|---|---|---|
| B1 | `shareWrite` weegt het deel-recht mee en laat de deel-kolommen ongeschreven als het sessielid ze niet mag wijzigen — dezelfde regel die de SharePicker al hanteerde, nu ook op het schrijfpad | `a7baf3f0` | ✅ Live |
| B3 | Het formulier levert altijd een volledig gematerialiseerde deellijst; de repository vouwt alleen nog op de resolver-tak. `liftShareListIntoVisibility` schaalt de stand mee met een niet-lege lijst | `a7baf3f0` | ✅ Live |
| B4 | Harde bovengrens op `updated_at` in `0046` (committijd van TEN-122) plus een waarschuwing waarom die grens load-bearing is | `a7baf3f0` | ✅ Live |
| B2 | Migratie `0083_panel_agenda_share_backfill.sql` met dezelfde tijdgrens | `f01f9ca0` | ✅ **Gedeployed 27-07 na eigenaar-toestemming** — zie hieronder |
| B5 | Nauwe vorm-gebonden uitzondering in `guard_chore_task_core` (migratie `0082`) zodat de klus-client een verse reeks lokaal kan zaaien, plus de client-kant die dan geen RPC meer aanroept | `408dce13` | 🟡 Migratie gedeployed 27-07; de client-helft werkt pas na een nieuwe build |

Bij B1 bleek onderweg dat het deel-recht **niet** bij constructie van de
formulier-state mag worden vastgelegd: het sessielid komt uit een stream die
tijdens `initState` nog niets heeft geëmit, dus dat zou op de eerste frame "geen
rechten" opleveren. Het recht is daarom een argument van `shareWrite`. Vier
sheet-tests legden dit bloot voordat het gedrag ergens anders kon opduiken.

## B2 — gedeployed op 27-07

De eigenaar gaf toestemming; migratie `0083` is tegen productie gedraaid.

**Telling vooraf:** 6 afspraken voldeden aan het predicaat, alle 6 met een
`updated_at` vóór de grens, 0 erna, 0 zonder tijdstempel. Er bestond dus nog geen
enkele bewuste privé-override op een agenda-afspraak — het schoonst denkbare
geval, en meteen het bewijs dat de tijdgrens hier niets nuttigs wegfilterde.

**Uitkomst:** 6 rijen omgezet naar `visibility = 2` met de betrokken leden in
`shared_with_member_ids`; nacontrole geeft 0 privé-afspraken met een ander
betrokken lid over. Geen nieuwe build nodig — de rijen komen via de gewone
sync-stream bij de betrokken leden binnen.

Migratie `0083` **verbreedde zichtbaarheid**: afspraken die privé stonden zijn nu
gedeeld met de leden waarvoor ze zijn gemaakt. Dat viel buiten de staande
toestemming (die dekt additieve kolom-migraties, niet data-remaps) en is daarom
apart voorgelegd. Wat er lag:

- Predicaat gespiegeld aan `0046`, met `updated_at < 1784972241` erbij.
- Een **negatieve controle** die aantoont dat juist die tijdgrens de bescherming
  is: zonder de regel wist de migratie een bewuste override (`UPDATE 2`), met de
  regel blijft die staan (`UPDATE 1`).
- Harnastest groen, her-uitvoeren is een no-op.

Resterende gaten, eerlijk benoemd:

- **Klok-skew.** `updated_at` komt van de *device*-klok, niet van de server. Een
  toestel dat achterloopt kan ná TEN-122 een echte override wegschrijven met een
  `updated_at` vóór de grens; die zou deze migratie alsnog wissen. De grens is
  exact tegen *codeversie*, niet tegen klokverschuiving.
- **Rijen met `updated_at IS NULL`** matchen niet en genezen dus niet. Dat is de
  veilige kant.
- **De grens is de committijd, niet de deploytijd** op de testtoestellen. Legacy-
  rijen die daartussen zijn aangeraakt genezen niet. Conservatief bedoeld.
- ~~**Geen productie-aantallen.**~~ Alsnog geteld vóór de deploy: 6 rijen, alle
  6 vóór de grens.
- **De rollback is een gedocumenteerde no-op**, in lijn met `0046_rollback.sql`:
  na de backfill is een omgezette rij niet te onderscheiden van een bewust
  gedeelde. Wil je terug kunnen, dan moet de set rij-ids **vóór** de deploy worden
  vastgelegd.

## B5 — gedeployed op 27-07, wacht nog op een build

Migratie `0082` staat op productie; geverifieerd tegen `pg_get_functiondef` dat
de zaai-uitzondering actief is **en** dat de "RPC-only"-regel eronder intact is
gebleven. De uitzondering is nauw: anker mag bewegen, mits de rij nog geen reeks
had (`old.recur_freq` null/0), de tellers op 0 stonden én niet meebewegen, de
nieuwe regel wél herhalend is, en de caller eigenaar of beheerder is. Alles
daarbuiten raist onveranderd 42501.

De **client-helft** (een verse reeks lokaal zaaien in plaats van de RPC bellen)
zit in `408dce13` maar draait pas na een nieuwe build op de toestellen. Tot dan
belt de oude client nog de RPC en blijft "eenmalige klus → wekelijks" falen. Er
is geen regressie: de RPC zelf is niet aangeraakt.

Bijvangst van deze vorm: het zaai-pad is nu één lokale schrijf zonder netwerk,
dus het werkt ook offline — waar de RPC-first-orde dat niet deed.

---
*Bronnen: `/tmp/panel-codex-review.md`, `/tmp/panel-glm-review.md`, `/tmp/panel-opus-review.md` (panel-rapporten, sessie 26-07); commits `a7baf3f0`, `f01f9ca0`; migraties `0044`, `0046`, `0052`, `0055`, `0077`, `0083`; `lib/core/domain/assignment_sharing.dart`, `lib/core/domain/mutation_guard.dart`. Geverifieerd op 2026-07-26.*
