feat(notes): Notes onder het Memory-menu (IDEA-226, PBI-167) #270

Merged
janpeter merged 26 commits from feat/notes-pbi-167 into main 2026-09-28 11:53:52 +02:00
Owner

Notes — privé snippets onder het Memory-menu (IDEA-226, PBI-167)

Sprint S-2026-09-28-1. Spec specs/idea-226-spec (r3), plan plans/idea-226-plan (r2).

Wat

  • Schema + contract (scrum4me-shared PR #65, gemerged 5f9afaa): Note, NoteKeyword, NoteKeywordLink; lib/note-schema.ts (limieten, countBodyChars, keyword-normalisatie, zod-schema's, veld-/ordercontracten, planKeywordResolution(userId, …)). Gitlink op shared main.
  • Migratie 20260928060000_add_notes: tabellen, partiële unieke index note_keywords_default_name_key, 10 defaults idempotent (ON CONFLICT DO NOTHING). Geen GRANTs — rechten komen uit de policy-apply-fase.
  • DB-access (T-1890): additieve contracten na de IDEA-213/227-adoptie in api-token-usage-transition, schema-preflight, provision; de drie notes-contracten in scripts/db-access/profiles/scrum4me.json.
  • Datalaag: lib/notes-server.ts is de enige prisma.note*-toegang; alle queries user-scoped, productaccess → "Product niet gevonden".
  • Actions: saveNoteAction/deleteNoteAction — auth 403 → demo 403 → rate limit → id/zod 422 → adapter.
  • UI: /notes met filters, pager, loading-skeleton; URL-gedreven NoteDialog (?newNote=1 / ?editNote=<id>), KeywordPicker, dialogprofiel docs/specs/dialogs/note.md.
  • Navigatie: Memory-dropdown (Issues, Ideas, Notes). /notes in protectedRoutes; proxy-demo-guard blijft /api/*-only (besluit JP) — demo-grens voor notes = action-403 + disabled knoppen.
  • Docs: docs/runbooks/notes-rollout.md, data-model, CLAUDE.md (drift-regel note_keywords, proxy-hardstop).
  • MCP-spiegel: PBI-30 in scrum4me-mcp (niet uitgevoerd).

Uitrol (volgorde, zie docs/runbooks/notes-rollout.md)

  1. shared ✅ gemerged. 2. Deze PR: policy-bundle/POLICY_HASH, dan web-deploy via de operator-route (server-DB); Neon handmatig. 3. scrum4me-workers submodule-bump. 4. MCP (PBI-30) als laatste.

Verificatie

npm run verify: lint 0 errors (7 bestaande warnings), typecheck groen, 3057 passed / 29 skipped. Schema-drift 0, npm run docs schoon. npm run build niet lokaal draaibaar in de worktree (env ontbreekt). Per-taak reviews + whole-branch review (opus) Approved na één fixronde.

Beslissingen (rulings)

  • MCP-listNotes weigert een productfilter buiten toegang/scope met "Product niet gevonden" (web filtert alleen op eigen notes) — spec § 2.5.
  • resolveKeywords blijft intern (pariteit met web).
  • Final review: Minor 3 (id-type-guard + rollback bij count-mismatch) en Minor 8 (runbook-SHA) meegenomen in de fixronde.

Uitgestelde minors

  • ?q=a&q=b geeft 500 op /notes; productfilter toont "Alle producten" bij gearchiveerd product; kopieer-toast meldt succes bij mislukte copy; titel/keyword tellen UTF-16, body codepoints; demo Cmd/Ctrl+Enter stuurt save (server 403).
  • Ongeteste payload-paden (gewijzigd product in edit, create stuurt product_id); product-fielderror blijft staan tot volgende submit; PRODUCT_NOT_FOUND_TEXT dupliceert servertekst.
  • Debounce niet geannuleerd bij externe navigatie; KeywordPicker geen Controller; NoteDialogProps geen discriminated union; Cmd/Ctrl+Enter in "Nieuw keyword" voegt toe én submit; startsWith-prefix in nav; proxy.md:96-97 formulering.

🤖 Generated with Claude Code

## Notes — privé snippets onder het Memory-menu (IDEA-226, PBI-167) Sprint S-2026-09-28-1. Spec `specs/idea-226-spec` (r3), plan `plans/idea-226-plan` (r2). ### Wat - **Schema + contract** (scrum4me-shared PR #65, gemerged `5f9afaa`): `Note`, `NoteKeyword`, `NoteKeywordLink`; `lib/note-schema.ts` (limieten, `countBodyChars`, keyword-normalisatie, zod-schema's, veld-/ordercontracten, `planKeywordResolution(userId, …)`). Gitlink op shared main. - **Migratie** `20260928060000_add_notes`: tabellen, partiële unieke index `note_keywords_default_name_key`, 10 defaults idempotent (`ON CONFLICT DO NOTHING`). Geen GRANTs — rechten komen uit de policy-apply-fase. - **DB-access (T-1890):** additieve contracten na de IDEA-213/227-adoptie in `api-token-usage-transition`, `schema-preflight`, `provision`; de drie notes-contracten in `scripts/db-access/profiles/scrum4me.json`. - **Datalaag:** `lib/notes-server.ts` is de enige `prisma.note*`-toegang; alle queries user-scoped, productaccess → "Product niet gevonden". - **Actions:** `saveNoteAction`/`deleteNoteAction` — auth 403 → demo 403 → rate limit → id/zod 422 → adapter. - **UI:** `/notes` met filters, pager, loading-skeleton; URL-gedreven `NoteDialog` (`?newNote=1` / `?editNote=<id>`), KeywordPicker, dialogprofiel `docs/specs/dialogs/note.md`. - **Navigatie:** Memory-dropdown (Issues, Ideas, Notes). `/notes` in `protectedRoutes`; proxy-demo-guard blijft `/api/*`-only (besluit JP) — demo-grens voor notes = action-403 + disabled knoppen. - **Docs:** `docs/runbooks/notes-rollout.md`, data-model, CLAUDE.md (drift-regel `note_keywords`, proxy-hardstop). - **MCP-spiegel:** PBI-30 in scrum4me-mcp (niet uitgevoerd). ### Uitrol (volgorde, zie `docs/runbooks/notes-rollout.md`) 1. shared ✅ gemerged. 2. Deze PR: policy-bundle/POLICY_HASH, dan web-deploy via de operator-route (server-DB); Neon handmatig. 3. scrum4me-workers submodule-bump. 4. MCP (PBI-30) als laatste. ### Verificatie `npm run verify`: lint 0 errors (7 bestaande warnings), typecheck groen, 3057 passed / 29 skipped. Schema-drift 0, `npm run docs` schoon. `npm run build` niet lokaal draaibaar in de worktree (env ontbreekt). Per-taak reviews + whole-branch review (opus) Approved na één fixronde. ### Beslissingen (rulings) - MCP-`listNotes` weigert een productfilter buiten toegang/scope met "Product niet gevonden" (web filtert alleen op eigen notes) — spec § 2.5. - `resolveKeywords` blijft intern (pariteit met web). - Final review: Minor 3 (id-type-guard + rollback bij count-mismatch) en Minor 8 (runbook-SHA) meegenomen in de fixronde. ### Uitgestelde minors - `?q=a&q=b` geeft 500 op `/notes`; productfilter toont "Alle producten" bij gearchiveerd product; kopieer-toast meldt succes bij mislukte copy; titel/keyword tellen UTF-16, body codepoints; demo Cmd/Ctrl+Enter stuurt save (server 403). - Ongeteste payload-paden (gewijzigd product in edit, create stuurt `product_id`); product-fielderror blijft staan tot volgende submit; `PRODUCT_NOT_FOUND_TEXT` dupliceert servertekst. - Debounce niet geannuleerd bij externe navigatie; KeywordPicker geen Controller; `NoteDialogProps` geen discriminated union; Cmd/Ctrl+Enter in "Nieuw keyword" voegt toe én submit; `startsWith`-prefix in nav; `proxy.md:96-97` formulering. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
De contracts-entries voor de drie notes-tabellen volgen apart: sinds IDEA-227
is scrum4me.json hash-gepind en een additief contract vraagt eerst een
wijziging in de db-access-code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sinds IDEA-227 is scripts/db-access/profiles/scrum4me.json byte-exact gepind,
waardoor geen enkele nieuwe tabel meer te deployen was (SOURCE_PIN_MISMATCH).
splitAdditiveContracts() haalt contracten buiten het gesloten idea-213-manifest
vóór de pins uit het profiel. Elk moet een nieuwe public-tabel zijn: sleutel niet
in legacy of idea-213-before, eigen migrationId (geen dispatch- of token-usage-
migratie), geen dispatch/projector- of PUBLIC-rechten en geen columnGrants.
Anders volgt een eigen DB_ACCESS_ADDITIVE_CONTRACT_*-code. De rest moet nog
steeds exact op targetProfileHash hashen; elke wijziging aan de gesloten set
blijft SOURCE_PIN_MISMATCH.

validateAdoptionPlan() is bewust niet versoepeld: cli.ts gaf hem al
transition.baseProfile, en dat is nu het gestripte profiel. De key-set-
vergelijking met het manifest blijft zo strikt. targetProfile blijft het
volledige profiel, zodat checkDatabase/applyContracts de nieuwe objecten exact
controleren en convergeren. permitsTokenUsageGrant laat additieve objecten buiten
de baseline-coverage; de volledige-profielcheck beoordeelt ze.

schema-preflight gebruikt dezelfde split vóór zijn pin. provision houdt additieve
migraties buiten de pre-adoptie-baseline (die brak al op de notes-migratie) en
migrateAdditive() draait ze na de adoptie in de token-usage- en preflight-suites.
Voegt de drie notes-contracten van T-1878 toe en documenteert de regel in
docs/runbooks/db-access-policy.md.

T-1890 / ST-1598 / PBI-167

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix round 1 (T-1890, Opus-review minors). De runbook belooft niet langer dat
elke wijziging aan de gesloten set SOURCE_PIN_MISMATCH geeft: de additieve
weigeringen draaien vóór de pin, dus een hernoemd gesloten contract of een
legacy-entry die naar contracts verhuist krijgt een ADDITIVE_CONTRACT_*-code.
Alle gevallen blijven fail-closed met exit 2. De tabel toont nu ook wat de
CLI werkelijk print (LEGACY_KEY en routine-sleutels -> DB_ACCESS_CONFIG_ERROR).
De pending-assert in token-usage-policy vergelijkt nu exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateNote relied on an earlier ownership findFirst inside the
transaction, then wrote via note.update({ where: { id } }) without
user_id. A note deleted concurrently between the two statements made
update() throw P2025 (500) instead of returning the same 404 as every
other not-found/not-yours path. The write itself now carries the scope
via note.updateMany({ where: { id, user_id } }); count !== 1 returns
the existing 404, and the keyword link delete/relink only run once
that scoped write has confirmed ownership.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full Vitest coverage for lib/notes-server.ts and actions/notes.ts per
task 8 (T-1881): user-scoping on every note/product/keyword query
(mutation-checked), body-contract, pagination/combined filters, full
§2.5.1 update semantics, resolveKeywords via createNote, and the
actions-layer 403/422/404 guards + revalidatePath.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix ronde 1 (reviewer-mutatiecheck): de herlees-findMany ná createMany in
resolveKeywords en tx.note.create.data.user_id waren de enige twee van de
negen user-scoped queries zonder eigen assertie. Beide nu mutatie-bewezen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
T-1882: server component app/(app)/notes/page.tsx met loading-skeleton,
NotesFilterBar (zoek + productselect + keyword-chips via URL-searchparams),
NoteList (rijen naar ?editNote=<id>, "Geen product"/lege staten, geen cuid
als label per ADR-0015) en NotesPager ("Vorige"/"Volgende" + "x-y van
total", filters blijven behouden). Component-tests voor de lege staten en
het behoud van filters in pager-links.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix round 1 op T-1882 (review: 2 CRITICAL, 1 IMPORTANT, 1 MINOR), TDD per
finding (rode test eerst):

- buildNotesHref stapelde newNote/editNote i.p.v. ze uit te sluiten — het
  zetten van de een wist nu expliciet de ander.
- NotesFilterBar's render-time q-sync verloor toetsaanslagen wanneer een
  server-rerender de zojuist zelf gepushte q bevestigde; een lastPushedQ-
  state (gezet in de debounce-callback, niet in een ref — react-hooks/refs
  verbiedt refs lezen tijdens render) onderscheidt nu "eigen push bevestigd"
  van "externe wijziging" (terug/vooruit, andere link).
- NotesPager toonde een omgekeerd bereik ("226–5 van 5") bij een page voorbij
  het einde met total>0; toont nu "0 van {total}", Vorige blijft werken.
- "Nieuwe note"-knop (render={<Link/>}) kreeg nativeButton={false}, conform
  het precedent in products/[id]/docs/[folder]/[slug]/page.tsx — voorkomt de
  Base UI dev-warning en de foutieve type="button" op de <a>.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix round 1 (T-1884): de keyword-dedup-test matchte per ongeluk de
beschikbare-chip 'git' i.p.v. de gekozen chip, waardoor hij vacuous was
(passeerde ook als addName() nooit normaliseert). Scoopt nu op
keyword-picker__chosen + within() en spiedt de daadwerkelijke keywords-array
via de saveNoteAction-call. Ook: docs/specs/dialogs/note.md liet 400 als
foutcode staan zonder toelichting dat Notes geen REST-route heeft — caveat
toegevoegd.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Server actions posten naar de URL van de aanroepende pagina, niet naar een
apart /api/*-pad. Een demo-guard op /notes zou daardoor ook layout-brede
actions blokkeren (logoutAction, product-/sprintswitcher) en was via een
encoded pad (/%6Eotes) te omzeilen. De proxy blokt weer uitsluitend
non-GET /api/*; /notes blijft in protectedRoutes (login verplicht), maar de
grens voor /notes-writes ligt bij de action-level isDemo-check (laag 2,
actions/notes.ts) — ongeacht het aanroeppad.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Minor 8 uit de final-review: de submodule stond al op 5f9afaa (PR #65,
zie chore(notes) b4b2c272), maar de runbook-tekst noemde nog de
ongemergede PR-tip fbe6c799. Verwijder ook de redundante
`> prisma/schema.prisma`-redirect — scripts/gen-schema.sh schrijft het
bestand al zelf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Minor 3 uit de final-review. Twee losstaande hardening-fixes:

- actions/notes.ts: `saveNoteAction`/`deleteNoteAction` valideren nu
  expliciet dat `id` een niet-lege string is vóór de adapter wordt
  aangeroepen. Server-action-argumenten zijn op de wire ongetypeerd —
  een aangepaste client kan `id` als object aanleveren (bv.
  `{ not: '' }`), wat anders ongefilterd in een Prisma `where`
  belandt.
- lib/notes-server.ts `updateNote`: een `count !== 1` in de scoped
  `updateMany` gooit nu binnen de `$transaction`-callback i.p.v. er
  stil uit te `return`en. Een `return` liet Prisma de transactie
  gewoon committen, dus een `updateMany` die (door zo'n type-confused
  id) meerdere rijen matchte zou zijn title/body-overschrijving laten
  staan terwijl de caller "niet gevonden" te horen kreeg.

Tests: __tests__/actions/notes.test.ts (id-type-confusion + lege
string, beide paden), __tests__/lib/notes-server.test.ts (count=2
rolt de transactie terug — bewezen via een reject van de
mock-$transaction-callback).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Important 2 uit de final-review. Een keyword langer dan
NOTE_KEYWORD_NAME_MAX (40) gaf een zod-fout op `keywords[i]` die
react-hook-form als een per-index array-fout terugzet, zonder
top-level `.message` op `errors.keywords` zelf. De render-conditie
keek alleen naar dat top-level `.message`, dus verscheen er niets —
de gebruiker klikte Save en zag geen enkele feedback.

- components/notes/note-dialog.tsx: `keywordsErrorMessage` valt terug
  op de eerste element-fout wanneer `errors.keywords` een array is
  (de max-10-array-fout blijft via het bestaande top-level `.message`
  werken).
- components/notes/keyword-picker.tsx: het invoerveld draagt nu
  `maxLength={NOTE_KEYWORD_NAME_MAX}`, geïmporteerd uit
  `@/lib/schemas/note` (geen hard-coded 40).

Test: __tests__/components/notes/note-dialog.test.tsx — een
41-teken keyword toont de foutmelding onder het keywords-veld en
`saveNoteAction` wordt niet aangeroepen; het invoerveld draagt
`maxLength={40}`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(notes): edit-note-dialog verliest invoer niet meer bij ontoegankelijk product
Some checks failed
CI / Lint, Typecheck, Test & Build (pull_request) Has been cancelled
CI / DB Access Policy Gate (pull_request) Has been cancelled
CI / Queue Dispatch Guard Gate (pull_request) Has been cancelled
CI / DB Access Production Sentinel (pull_request) Has been cancelled
CI / Detect deploy-relevant changes (pull_request) Has been cancelled
CI / Deploy Preview (PR) (pull_request) Has been cancelled
CI / Deploy Production (main) (pull_request) Has been cancelled
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been cancelled
e4a004f7e5
Important 1 uit de final-review — drie samenhangende deelfixes op
components/notes/note-dialog.tsx:

a) Edit-mode stuurt `product_id` alleen mee als het veld is aangeraakt
   (`form.formState.dirtyFields.product_id`). `noteUpdateSchema`
   behandelt een weggelaten veld als ongewijzigd (spec § 2.5.1), dus
   een note met een inmiddels-ontoegankelijk gelinkt product (de
   gebruiker is uit dat product-team gezet, of het is gearchiveerd)
   kan weer worden opgeslagen zolang het product zelf niet wijzigt.
b) De producttrigger valt terug op `note.product.name` in plaats van
   "Geen product" wanneer het gelinkte product niet in de
   selecteerbare `products`-lijst zit — anders leek het alsof er nooit
   een product gekoppeld was.
c) Een 404 "Product niet gevonden" wordt nu een veldfout op
   `product_id` (zelfde behandeling als een 422-fieldError, dialog.md
   §5.3): de dialoog blijft open en de getypte titel/body blijven
   staan. Alleen "Note niet gevonden" sluit de dialoog nog — dat is
   het enige geval waarin de note zelf weg is.

Test: __tests__/components/notes/note-dialog.test.tsx, drie nieuwe
gevallen onder een note met een product buiten de toegankelijke lijst
— (a) saveNoteAction krijgt geen `product_id` als het veld ongewijzigd
bleef, (b) de trigger toont de productnaam i.p.v. "Geen product", (c)
een 404 Product-niet-gevonden-respons toont de veldfout zonder de
dialoog te sluiten of de invoer te wissen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
s4m-codex-reviewer requested changes 2026-09-28 11:11:38 +02:00
Dismissed
s4m-codex-reviewer left a comment

Verdict: REQUEST_CHANGES

Findings

  • error — proxy.ts:16 — De nieuwe demo-guard begrenst middlewareblokkering expliciet tot /api/ (return pathname.startsWith('/api/')). Next Server Actions voor saveNoteAction en deleteNoteAction zijn POSTs naar /notes met de Next-Action-header en vallen daardoor buiten deze laag. Daarmee bevat de Notes-writeflow slechts de UI- en action-check, terwijl de verplichte drielaagse demo-policy uit de dialog-standaard een proxy/middleware-guard voor elke write vereist. Breid de guard uit voor Server Actions (of routeer de writes via een beschermde API-route) en voeg een regressietest voor een demo-POST met Next-Action toe.

De overige beoordeelde Notes-contracten (user-scoping, body-uitsluiting uit lijstqueries, schema/migratie, navigatie en documentatie) sluiten aan op het gekoppelde plan en de productstandaarden.

## Verdict: REQUEST_CHANGES ### Findings - **error — proxy.ts:16** — De nieuwe demo-guard begrenst middlewareblokkering expliciet tot `/api/` (`return pathname.startsWith('/api/')`). Next Server Actions voor `saveNoteAction` en `deleteNoteAction` zijn POSTs naar `/notes` met de `Next-Action`-header en vallen daardoor buiten deze laag. Daarmee bevat de Notes-writeflow slechts de UI- en action-check, terwijl de verplichte drielaagse demo-policy uit de dialog-standaard een proxy/middleware-guard voor elke write vereist. Breid de guard uit voor Server Actions (of routeer de writes via een beschermde API-route) en voeg een regressietest voor een demo-POST met `Next-Action` toe. De overige beoordeelde Notes-contracten (user-scoping, body-uitsluiting uit lijstqueries, schema/migratie, navigatie en documentatie) sluiten aan op het gekoppelde plan en de productstandaarden.
docs(demo): ADR-0006 en dialog-patroon volgen de echte proxy-scope (/api/*)
All checks were successful
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 6m38s
CI / DB Access Policy Gate (pull_request) Successful in 1m52s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 2m13s
CI / DB Access Production Sentinel (pull_request) Successful in 1m41s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
c2c2bfb52f
De proxy-demo-guard dekte altijd alleen /api/*; de ADR- en dialogtekst
beloofden een guard vóór elke server action. Voor pagina-server-actions is
de isDemo-check in de action de handhavende grens (besluit JP, IDEA-226).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Reactie op de REQUEST_CHANGES (proxy.ts:16) — bewust niet uitgebreid, besluit JP; de norm waarop de bevinding steunt is in c2c2bfb5 rechtgezet.

Waarom geen proxy-guard voor server actions: een server action is een POST naar de URL van de pagina waarop hij draait, met een opaque Next-Action-id. Een guard op /notes blokkeert daarom ook logoutAction en andere layout-actions die vanaf die pagina posten — de proxy kan ze niet onderscheiden. Die variant is in deze PR gebouwd (22faaf89) en in review juist daarom teruggedraaid (8bc8b0d0); JP koos 'proxy-laag weg voor pagina-actions'.

De norm klopte nooit met de code: op main vóór deze PR (1d766431) matchte de demo-guard al alleen pathname.startsWith('/api/'). Geen enkele bestaande dialog met server actions (issues, ideas, …) had dus een laag 1. ADR-0006 en docs/patterns/dialog.md §6 beloofden 'alle non-GET vóór elke server action'; beide zijn nu bijgewerkt: laag 1 = /api/*, voor server actions is laag 2 de handhavende grens, laag 3 blijft verplicht.

Demo-bescherming van de notes-writes: saveNoteAction/deleteNoteAction weigeren demo met 403 vóór elke write (guard-volgorde auth → demo → rate limit → validatie; tests __tests__/actions/notes.test.ts:41 en :143), en de knoppen zijn disabled met DemoTooltip. De gevraagde regressietest met Next-Action bestaat: __tests__/proxy/demo-guard.test.ts:120 legt vast dat de proxy zo'n POST doorlaat en waarom.

Reactie op de REQUEST_CHANGES (proxy.ts:16) — bewust niet uitgebreid, besluit JP; de norm waarop de bevinding steunt is in c2c2bfb5 rechtgezet. **Waarom geen proxy-guard voor server actions:** een server action is een `POST` naar de URL van de pagina waarop hij draait, met een opaque `Next-Action`-id. Een guard op `/notes` blokkeert daarom ook `logoutAction` en andere layout-actions die vanaf die pagina posten — de proxy kan ze niet onderscheiden. Die variant is in deze PR gebouwd (22faaf89) en in review juist daarom teruggedraaid (8bc8b0d0); JP koos 'proxy-laag weg voor pagina-actions'. **De norm klopte nooit met de code:** op `main` vóór deze PR (1d766431) matchte de demo-guard al alleen `pathname.startsWith('/api/')`. Geen enkele bestaande dialog met server actions (issues, ideas, …) had dus een laag 1. ADR-0006 en `docs/patterns/dialog.md` §6 beloofden 'alle non-GET vóór elke server action'; beide zijn nu bijgewerkt: laag 1 = `/api/*`, voor server actions is laag 2 de handhavende grens, laag 3 blijft verplicht. **Demo-bescherming van de notes-writes:** `saveNoteAction`/`deleteNoteAction` weigeren demo met 403 vóór elke write (guard-volgorde auth → demo → rate limit → validatie; tests `__tests__/actions/notes.test.ts:41` en `:143`), en de knoppen zijn disabled met `DemoTooltip`. De gevraagde regressietest met `Next-Action` bestaat: `__tests__/proxy/demo-guard.test.ts:120` legt vast dat de proxy zo'n POST doorlaat en waarom.
s4m-codex-reviewer left a comment

APPROVED

Findings

  • Geen blokkerende findings. De implementatie is plan-conform: user-scoping en producttoegangscontrole zitten in de adapter, het bodycontract wordt via de gedeelde veldcontracten bewaakt, en de URL-gebaseerde dialog, demo-poorten, Memory-navigatie, migratie/db-access-contracten en regressietests zijn aanwezig.
## APPROVED ### Findings - Geen blokkerende findings. De implementatie is plan-conform: user-scoping en producttoegangscontrole zitten in de adapter, het bodycontract wordt via de gedeelde veldcontracten bewaakt, en de URL-gebaseerde dialog, demo-poorten, Memory-navigatie, migratie/db-access-contracten en regressietests zijn aanwezig.
janpeter scheduled this pull request to auto merge when all checks succeed 2026-09-28 11:29:07 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
janpeter/Scrum4Me!270
No description provided.