feat(issues): issue-tracker per product of systeem met Forgejo-mirror #174

Merged
janpeter merged 32 commits from claude/issue-tracker-per-product-4720ca into main 2026-08-17 08:57:22 +02:00
Owner

Implementatie van de issue-tracker per product of systeem. Spec docs/superpowers/specs/2026-08-16-issue-tracker-design.md (dubbel-GO, 7 reviewrondes) en plan docs/superpowers/plans/2026-08-16-issue-tracker.md (dubbel-GO, 9 rondes), sprint S-2026-08-17-1 / PBI-143, 16 taken over 7 fasen — allemaal afgerond.

Waarom

De problemen die op max2 en scrum4me-server optreden hadden nergens een plek. Ze worden nu Product-rijen met kind = SYSTEM en krijgen dezelfde tracker als een app: registratie, onderzoek, oplossing — en een one-way spiegel naar Forgejo zodat ze daar zichtbaar zijn naast het gewone werk.

Wat er landt

Data. Issue + IssueLog (shared-first, submodule op c63f0bd) met migratie 20260817090000_issue_tracker. Twee invarianten staan bewust in de database:

  • de CLOSED-CHECK bindt status, resolution en closed_at aan elkaar — een gesloten issue zonder afhandeling is geen geldige toestand voor welk schrijfpad dan ook;
  • issues_link_loss_dirty vangt wat de applicatie per definitie niet ziet: een PBI- of Idea-delete zet linked_*_id via ON DELETE SET NULL op NULL buiten alle app-code om. Zonder die trigger blijft de spiegel schoon-maar-verouderd achter, en dus ook buiten bereik van de sweep.

De partial unique index op open fingerprints maakt dedup race-bestendig.

Mirror. Full-state en self-healing. Drie dingen dragen de correctheid: een session-scoped advisory lock op een pooler-vrije verbinding (hij moet de HTTP-calls overspannen, dus Prisma's transactiedeadline is te kort), alle render-data uit de read ná die lock, en een CAS op forgejo_sync_seq die alleen opschoont als de rij nog dezelfde versie draagt. Bij een botsing blijft dirty staan en blijft attempted_at onaangeroerd, zodat de sweep het issue meteen weer kiest. De boekhouding gaat via een pg-client en niet via Prisma — @updatedAt zou de changed_fields-diff vervuilen waar de badge-only-tak op stuurt.

Invarianten rond de mirror. De spiegel rendert meer dan het issue zelf: productnaam, soort en PBI-code. Een wijziging daar verandert dus de gespiegelde staat van issues die niemand heeft aangeraakt. updateProductWithMirrorInvariant is nu het enige schrijfpad voor product-updates (alle vier de actions, bewaakt door een wiring-test) en updatePbiAction doet hetzelfde voor Pbi.code. Beide vergelijken tegen de verse in-transactie-rij onder Serializable: onder READ COMMITTED zou een stale formulierwaarde "geen wijziging" kunnen concluderen terwijl er wél iets veranderde.

Cron-gating. enqueue-docs-audit en dispatch-reviews filteren op kind: 'APP'. Een SYSTEM-product draagt de gedeelde infra-repo; daar hoort geen docs-audit of PR-review op los te gaan.

UI. Globale lijst op /issues met filters en realtime, plus een detailpagina waar de drie secties inline bewerken. Sluiten heeft een eigen dialoog omdat er een verplichte keuze bij hoort. /issues staat in protectedRoutes — zonder die regel was de pagina publiek.

Realtime. Eigen SSE-route (geen product_id-parameter; server-side gefilterd op de accessible-set), solo weert issue-events expliciet, en de hook coalesceert her-fetches binnen 250 ms zodat een sweep over N issues één fetch oplevert.

Bijbehorende PR's

  • scrum4me-shared #51 + #52 — gemerged, submodule staat op c63f0bd.
  • scrum4me-mcp #117create_issue / update_issue / list_issues / get_issue + inline-sync. Nog open; de merge en de mcp-stable-release staan in het rollout-runbook.

Verificatie

npm run verify groen: 264 testbestanden, 2108 tests, waarvan 58 nieuw. npm run docs groen (218 docs, 177 bestanden linkcheck). npm run build is in een worktree bekend onhaalbaar (prisma-CLI in prebuild) en is niet gedraaid.

Nog te doen — niet in deze PR

  • Security-review van spec §10. Beide GO's dekken correctheid; de security-lens is in alle 16 reviewrondes bewust overgeslagen per staande instructie. De input staat in §10.
  • Rollout, inclusief de outward-facing stappen die JP zelf doet: docs/runbooks/issue-tracker-rollout.md. De agent-users als ProductMember zetten is daar een preconditie, geen detail — zonder dat weigert userCanAccessProduct élke create_issue.
  • De Neon-migratie is handmatig (de build draait geen migrate deploy).

🤖 Generated with Claude Code

Implementatie van de issue-tracker per product of systeem. Spec [`docs/superpowers/specs/2026-08-16-issue-tracker-design.md`](docs/superpowers/specs/2026-08-16-issue-tracker-design.md) (dubbel-GO, 7 reviewrondes) en plan [`docs/superpowers/plans/2026-08-16-issue-tracker.md`](docs/superpowers/plans/2026-08-16-issue-tracker.md) (dubbel-GO, 9 rondes), sprint `S-2026-08-17-1` / PBI-143, 16 taken over 7 fasen — allemaal afgerond. ## Waarom De problemen die op max2 en scrum4me-server optreden hadden nergens een plek. Ze worden nu Product-rijen met `kind = SYSTEM` en krijgen dezelfde tracker als een app: registratie, onderzoek, oplossing — en een one-way spiegel naar Forgejo zodat ze daar zichtbaar zijn naast het gewone werk. ## Wat er landt **Data.** `Issue` + `IssueLog` (shared-first, submodule op `c63f0bd`) met migratie `20260817090000_issue_tracker`. Twee invarianten staan bewust in de database: - de CLOSED-CHECK bindt status, resolution en `closed_at` aan elkaar — een gesloten issue zonder afhandeling is geen geldige toestand voor welk schrijfpad dan ook; - `issues_link_loss_dirty` vangt wat de applicatie per definitie niet ziet: een PBI- of Idea-delete zet `linked_*_id` via `ON DELETE SET NULL` op NULL buiten alle app-code om. Zonder die trigger blijft de spiegel schoon-maar-verouderd achter, en dus ook buiten bereik van de sweep. De partial unique index op open fingerprints maakt dedup race-bestendig. **Mirror.** Full-state en self-healing. Drie dingen dragen de correctheid: een session-scoped advisory lock op een pooler-vrije verbinding (hij moet de HTTP-calls overspannen, dus Prisma's transactiedeadline is te kort), alle render-data uit de read ná die lock, en een CAS op `forgejo_sync_seq` die alleen opschoont als de rij nog dezelfde versie draagt. Bij een botsing blijft dirty staan en blijft `attempted_at` onaangeroerd, zodat de sweep het issue meteen weer kiest. De boekhouding gaat via een pg-client en niet via Prisma — `@updatedAt` zou de `changed_fields`-diff vervuilen waar de badge-only-tak op stuurt. **Invarianten rond de mirror.** De spiegel rendert meer dan het issue zelf: productnaam, soort en PBI-code. Een wijziging daar verandert dus de gespiegelde staat van issues die niemand heeft aangeraakt. `updateProductWithMirrorInvariant` is nu het enige schrijfpad voor product-updates (alle vier de actions, bewaakt door een wiring-test) en `updatePbiAction` doet hetzelfde voor `Pbi.code`. Beide vergelijken tegen de verse in-transactie-rij onder Serializable: onder READ COMMITTED zou een stale formulierwaarde "geen wijziging" kunnen concluderen terwijl er wél iets veranderde. **Cron-gating.** `enqueue-docs-audit` en `dispatch-reviews` filteren op `kind: 'APP'`. Een SYSTEM-product draagt de gedeelde infra-repo; daar hoort geen docs-audit of PR-review op los te gaan. **UI.** Globale lijst op `/issues` met filters en realtime, plus een detailpagina waar de drie secties inline bewerken. Sluiten heeft een eigen dialoog omdat er een verplichte keuze bij hoort. `/issues` staat in `protectedRoutes` — zonder die regel was de pagina publiek. **Realtime.** Eigen SSE-route (geen `product_id`-parameter; server-side gefilterd op de accessible-set), solo weert issue-events expliciet, en de hook coalesceert her-fetches binnen 250 ms zodat een sweep over N issues één fetch oplevert. ## Bijbehorende PR's - `scrum4me-shared` **#51** + **#52** — gemerged, submodule staat op `c63f0bd`. - `scrum4me-mcp` **#117** — `create_issue` / `update_issue` / `list_issues` / `get_issue` + inline-sync. Nog open; de merge en de mcp-stable-release staan in het rollout-runbook. ## Verificatie `npm run verify` groen: 264 testbestanden, 2108 tests, waarvan 58 nieuw. `npm run docs` groen (218 docs, 177 bestanden linkcheck). `npm run build` is in een worktree bekend onhaalbaar (prisma-CLI in prebuild) en is niet gedraaid. ## Nog te doen — niet in deze PR - **Security-review van spec §10.** Beide GO's dekken correctheid; de security-lens is in alle 16 reviewrondes bewust overgeslagen per staande instructie. De input staat in §10. - **Rollout**, inclusief de outward-facing stappen die JP zelf doet: [`docs/runbooks/issue-tracker-rollout.md`](docs/runbooks/issue-tracker-rollout.md). De agent-users als ProductMember zetten is daar een preconditie, geen detail — zonder dat weigert `userCanAccessProduct` élke `create_issue`. - **De Neon-migratie** is handmatig (de build draait geen `migrate deploy`). --- 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Issue-entiteit per product, systemen als Product.kind=SYSTEM (max2,
scrum4me-server), MCP-tools met fingerprint-dedup, one-way full-state
Forgejo-mirror. Onderbouwd met websearch (lifecycle/resolution-consensus,
Sentry-dedup, gl-infra-ops-patroon) en codebase-verkenning; schema-uitbreiding
vooraf gevalideerd met prisma validate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hub_permission_rules en Documents-projector bestaan (nog) niet in deze tree;
vervangen door copilot_jobsource_constraint (CHECK),
agent_message_idempotency_key (partial unique index) en actions/deploy.ts
(pg_advisory_xact_lock). Migratie-vormtest-precedent naar __tests__/db.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Realtime: eigen SSE-route /api/realtime/issues met lokale typering i.p.v.
uitbreiding van de job-event-union; session-scoped try-advisory-lock i.p.v.
xact-lock rond de Forgejo-calls; volledig sweep-predicaat + ordening + noodrem;
dirty-transactie-invariant; link-invariant PBI/Idea; agent-toegang tot
SYSTEM-producten in rollout + E2E-preconditie; drie minors. Review record ronde 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CAS-afronding via forgejo_sync_seq (stale sync kan dirty niet meer wissen),
sweep op forgejo_attempted_at met 30-min-backoff, session-lock op eigen
pooler-vrije pg-client (web DIRECT_URL zonder fallback; MCP optionele
DIRECT_URL-host-env + src/db/session-lock.ts), product-niveau dirty-bulk,
E2E-gate (e) op de noodrem + nieuwe (f) échte-fout-test, solo-route-wering.
Schema-uitbreiding opnieuw gevalideerd. Review record ronde 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Product-dirty via één verplichte transactionele helper over alle vier de
name/repo_url-schrijfpaden (incl. admin; discrepantie tussen reviewers zelf
beslecht: vier paden, geverifieerd); attempted_at-semantiek gerepareerd
(CAS-miss = direct weer sweep-kandidaat; backoff alleen sweep-scheduling);
hook filtert pure forgejo_*-boekhouding; DIRECT_URL in de §13.5-verificatie +
zichtbaar web-faalpad; vijf i.p.v. zes MCP-locks; ORDER BY vereenvoudigd.
Review record ronde 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NOTIFY-payload draagt nu forgejo_dirty/number/error zodat de sync-badge
client-side kan (uitbreiden is vóór de migratie gratis, daarna twee
databases); Product.kind toegevoegd aan de helper-triggerset (bepaalt
titelformaat + host-label, blijft in edit aanpasbaar); helper-snippet met
echte veldnamen; badge-test in beide richtingen + kind-wissel-test.
Review record ronde 4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Helper-bulk als ruwe SQL mét WHERE product_id (codex: updateMany zonder
where raakt alles); alle forgejo_*-boekhouding via $executeRaw die
updated_at ongemoeid laat (claude: @updatedAt zou changed_fields vervuilen
en de badge-tak doodleggen); §7-tiebreaker van updated_at naar nieuw veld
closed_at (schema hergevalideerd); forgejo_repo in de NOTIFY-payload voor
het linkicoon; forgejo_error-op-NOTIFY als expliciet security-review-input.
Review record ronde 5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Badge-only-set expliciet zónder forgejo_sync_seq: dat veld beweegt alleen in
mutatietransacties, dus helper-bulk-events dwingen altijd een volledige
her-fetch af — sluit claude's stale-scherm-MAJOR én codex' geen-doelrepo-MAJOR
met één discriminator (codex' genoemde alternatief). closed_at in de dubbele
DB-CHECK + ORDER BY closed_at DESC NULLS LAST + statusmachinetest.
Review record ronde 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex 0/0/0 GO, claude 0/0/4 GO op rev 8. Minors op aanbeveling van de
reviewer zonder extra ronde verwerkt: closed_at ook gewist in het
§7-regressiepad, §13.4 noemt kind, her-fetch-coalescing (~250 ms),
SYNC-log-lag gedocumenteerd. GO dekt correctheid/uitvoerbaarheid;
security-review volgt apart vóór bouwen (§10 + Review record).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shared-first (schema+mappers+mirror-render), migratie met NOTIFY-trigger,
web-actions met dirty/link/transitie-invarianten, mirror-executor met
session-lock+CAS, sweep-cron, SSE+hook met badge-discriminator, lijst/detail-UI,
MCP-tools met fingerprint-dedup, rollout-runbook. TDD per task.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cron-gating kind=APP als Task 7 Step 5 (spec §5-gat, codex); trigger-SQL naar
de bewezen IF/ELSE-vorm (claude: OLD niet toegekend bij INSERT); SSE-filter
naar eigen module i.p.v. route-export (Next valideert route-exports);
executor-verwerpt-nooit-contract + .catch op alle aanroepplaatsen;
rate-limit-scopes in CONFIGS; bestaande MD3-tokens; product_has_repo echt
berekend; dialog.md §12. Review record ronde 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror representeert nu links + archivering (render-relevant per §6) via de
mappers; solo-union + wering als samenhangende TS-fix; Task-7-testharnas
gemigreerd + wiring-tests over alle vier de actions + scope-parameter-assert;
SCRUM4ME_BASE_URL met default als bron voor de body-link; .env.example
uitgecommentarieerd + z.string() (crash-op-kopie weg); list_issues nieuwste
eerst conform §8; heropening logt STATUS_CHANGE + REOPENED. Review record r2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stale-render-CAS-race dicht: voorcontroles zonder render-data, volledige
projectie + seq-snapshot uit één verse post-lock-read, race-testcase in
web én MCP (codex M1). PBI-code-lag als bewuste bekende lag gedocumenteerd;
DEFAULT_APP_BASE_URL één bron in de shared module (claude m1/m2, GO).
Review record ronde 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PBI-code-mirror-invariant: markLinkedIssuesDirtyForPbi transactioneel in
updatePbiAction (codex: gedocumenteerde lag was geen convergentie); expliciete
MCP-testlijst incl. race-case; dubbele preconditie-zinnen weg; post-lock-guard
op repo_url herhaald (skipped zonder error); unlock alleen bij verkregen lock.
Review record ronde 4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
issues_link_loss_dirty BEFORE-trigger in de migratie (JP-besluit): SetNull-
cascades van PBI-/Idea-deletes zetten dirty+seq DB-hard, GREATEST bewaart
app-increments — dekt alle drie de delete-paden en toekomstige; vormtest-
asserts. updatePbiAction hergebruikt de geladen pbi.code (r110-175).
Regressiecases 10/11 (post-lock-guard, lock-cleanup) in web én MCP.
Review record ronde 5 incl. JP-tussenbesluit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
update_issue krijgt authored_by?-inputveld; append-scheider tekent met
authored_by ?? token-username, nooit Issue.reported_by (claude M1: melder is
niet de schrijver) — vastgelegd als plan-lezing (c). Vormtest assert nu ook
RETURN NEW + dirty-assignment. Codex-ronde-6 faalde op MCP-transport
(infrafout, geen verdict) — ronde 7 her-toetst. Review record ronde 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Effectieve-wijziging-checks nu Serializable met verse in-transactie-read +
P2034-retry (runSerializableTransaction-precedent) — sluit codex' race;
vervangt het r5-hergebruik van de geladen pbi.code. Fase 6 werkt in een
verse clone van origin (dev-checkout hernoemd naar .disabled-20260816);
mcp-stable-releasestap kreeg een herstel-preconditie (JP-beslissing).
authored_by met zod-vorm trim/min(1)/max(60). Review record ronde 7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vi.hoisted()-harnas (TDZ-fix, repo-conventie); echte versheids-regressie
(poging 1 draait met stale 'A', P2034 bij commit, retry leest 'B') voor
product én PBI; edit-modus behoudt SYSTEM (kind via caller + form-defaults +
regressietest); verse-clone-bootstrap met --recurse-submodules.
Review record ronde 8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex 0/0/0 GO, claude 0/0/1 GO op rev 9. Minor zonder extra ronde verwerkt:
pbi-action-cases (c)/(d) naar eigen __tests__/actions/pbis-mirror.test.ts met
het products.test-harnas. Eindstand: spec 7 + plan 9 = 16 reviewrondes.
HARDSTOP: JP-gate vóór ceremonie; security-review en mcp-stable-herstel open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Haalt de issue-tracker-modellen uit scrum4me-shared (merge 77a136b) binnen en
zet ze in de database.

Twee invarianten staan bewust in de DB en niet alleen in de applicatie:

- de CLOSED-CHECK bindt status, resolution en closed_at aan elkaar. Een
  gesloten issue zónder resolution is geen geldige toestand, en dat mag geen
  enkel schrijfpad kunnen produceren — ook niet een toekomstig pad dat de
  server-action omzeilt.
- issues_link_loss_dirty vangt wat de applicatie per definitie niet ziet: een
  PBI- of Idea-delete zet linked_*_id via ON DELETE SET NULL op NULL buiten
  alle app-code om. Zonder deze trigger zou de Forgejo-spiegel schoon-maar-
  verouderd achterblijven, en dus ook geen sweep-kandidaat meer zijn.

De partial unique index op open fingerprints maakt dedup race-bestendig: er
kan hooguit één niet-gesloten issue per (product, fingerprint) bestaan, dus de
verliezer van een insert-race valt vanzelf terug op het occurrence-pad.

De NOTIFY-trigger draagt de forgejo_*-velden mee zodat de UI op een enkele
badge-verversing kan uitkomen zonder her-fetch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Issue-codes lopen per product, niet globaal: ISS-1 in Scrum4Me en ISS-1 op
max2 zijn verschillende issues. De counter leeft daarom op Product en de
generator is self-correcting — hij vergelijkt de counter met de hoogste
bestaande code en her-synct als die achterloopt, zodat een geïmporteerde of
handmatig ingevoerde reeks geen dubbele code kan opleveren.

Geen padding (ISS-27, niet ISS-027): dezelfde vorm als sprintcodes, en het
scheelt een format-beslissing zodra een product voorbij de honderd gaat.

De zod-schemas spreken API-lowercase, met de enumwaarden uit de gedeelde
mappers. Zo kan er geen tweede spelling van "gesloten" ontstaan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Anders dan Idea zijn issues team-zichtbaar: toegang loopt via
product-membership, niet via de eigenaar. Iedereen die het product mag zien
mag het probleem zien.

De update-action bewaakt drie dingen die elders niet af te dwingen zijn:

- transities gaan via canTransitionIssue, en sluiten vereist een resolution.
  Een resolution zónder close-transitie is een 422, want dat zou een open
  issue met een opgeloste-vlag opleveren.
- links moeten binnen hetzelfde product blijven. Voor ideeën telt ook een
  IdeaProduct-rij, omdat een idee meerdere producten kan raken; een
  productloos idee koppelen kan niet.
- archiveren kan alleen op een gesloten issue.

Elke mutatie zet forgejo_dirty en hoogt forgejo_sync_seq op in dezelfde
transactie. Dat is het hele fundament van de spiegel: een issue dat wijzigt
is per definitie vuil, en de seq laat de sync-executor straks zien of hij nog
over de actuele versie beschikt.

lib/issue-sync-server.ts landt hier bewust als een executor die niets doet.
Dat is geen placeholder maar het gedegradeerde gedrag: dirty blijft staan en
de repair-sweep pakt het op. Task 8 vult de body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
De Forgejo-spiegel rendert meer dan de issue zelf: de productnaam en het soort
bepalen de titel en de labels, en de PBI-code staat in de metadata. Een
wijziging daar verandert dus de gespiegelde staat van issues die zelf niet
zijn aangeraakt. Zonder markering blijven die schoon-maar-verouderd achter —
en daarmee ook buiten bereik van de repair-sweep.

updateProductWithMirrorInvariant is nu het enige schrijfpad voor
product-updates; alle vier de bestaande actions gaan erdoorheen en een
wiring-test bewaakt dat. De vergelijking draait Serializable met de verse
in-transactie-rij: onder READ COMMITTED zou een stale formulierwaarde
"geen wijziging" kunnen concluderen terwijl er wel degelijk iets veranderde.
Dezelfde redenering geldt voor Pbi.code, vandaar dat updatePbiAction de code
binnen de transactie opnieuw leest in plaats van de al geladen rij te
hergebruiken.

Product.kind komt in schema en dialog. In edit-modus wordt de bestaande
waarde geïnitialiseerd én gereset, zodat een gewone naamswijziging op max2
niet stilletjes SYSTEM naar APP terugzet.

De twee crons die repo_url als code-repo behandelen filteren voortaan op
kind = APP. Een SYSTEM-product draagt de gedeelde infra-repo; daar hoort geen
docs-audit of PR-review op los te gaan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
De spiegel is one-way en full-state, dus correctheid zit in drie details:

De advisory lock is session-scoped op een pooler-vrije verbinding. Hij moet de
HTTP-calls naar Forgejo overspannen en Prisma's transactiedeadline is daar te
kort voor. Bij een niet-verkregen lock stoppen we zonder unlock — unlocken
zonder lock geeft een Postgres-WARNING per keer, precies op het pad waar sweep
en inline-sync elkaar correct passeren.

Alle render-data komt uit de read ná de lock, inclusief de seq-snapshot. De
voorcontrole leest bewust geen rendervelden; hij bespaart alleen een lock voor
rijen die evident niets te spiegelen hebben. Zou de render uit die pre-read
komen, dan kon een mutatie in het venster ertussen ongespiegeld blijven
terwijl de CAS tóch slaagt.

De CAS op forgejo_sync_seq schoont alleen op als de rij nog dezelfde versie
draagt als wat we hebben gepusht. Bij een botsing blijft dirty staan en blijft
attempted_at onaangeroerd, zodat de sweep het issue meteen weer kiest in
plaats van na de backoff.

De boekhouding gaat via de pg-client, niet via Prisma: @updatedAt zou de
changed_fields-diff vervuilen waar de badge-only-tak op stuurt.

Eén try/catch omspant de hele body. De executor wordt als floating promise
aangeroepen vanuit server-actions; een unhandled rejection kan daar het
proces beëindigen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inline-sync is opportunistisch: hij kan de lock missen, een netwerkfout raken
of tegen een stale CAS aanlopen. Deze sweep is het gegarandeerde pad — een
issue dat dirty is en een product met repo heeft komt er vroeg of laat
doorheen.

De ordening is langst-niet-geprobeerd eerst, met NULL vooraan zodat een
gloednieuw issue niet achteraan sluit achter rijen die al een poging achter de
rug hebben. De backoff van 30 minuten voorkomt dat een structureel falend
issue elke ronde de cap opeet.

Zonder FORGEJO_TOKEN is de route een nette no-op in plaats van een fout: op de
Vercel/Neon-deploy bestaat die env niet, en dat is geen storing maar de
bedoeling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
De issues-lijst is globaal en team-zichtbaar, dus deze route kent geen
product_id-parameter: hij bepaalt de toegankelijke producten van de sessie
vooraf en filtert daar server-side op. Het filter zit in een eigen module —
Next valideert route-exports, dus een helper-export uit het route-bestand is
een typefout die pas bij de echte build opduikt.

Solo weert issue-events expliciet. Dat is filter-discipline: elk kanaal
benoemt wat het níet doorlaat, zodat een nieuwe entity niet stilzwijgend in
een scherm belandt waar hij niet hoort.

De hook coalesceert her-fetches binnen 250 ms, zodat een helper-bulk over N
issues één fetch oplevert in plaats van N. Pure sync-boekhouding werkt alleen
de badge bij; omdat forgejo_sync_seq bewust buiten die set valt, verraadt zijn
aanwezigheid in changed_fields dat er écht iets gemuteerd is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
De lijst is globaal in plaats van per product: de problemen op max2 en
scrum4me-server staan naast die van de apps, en je filtert erop in plaats van
eerst een product te moeten kiezen. Gesloten issues staan standaard uit —
zonder dat vult de lijst zich vanzelf met historie.

De sync-kolom is de plek waar de spiegel zichtbaar wordt: een link naar het
Forgejo-issue als er een nummer is, en anders waaróm er geen link is (geen
doelrepo, een fout, of nog niet gesynct). Die kolom werkt bij op
boekhoudings-events zonder de hele pagina te verversen; een sweep over veel
issues zou anders evenzoveel refreshes veroorzaken.

De route staat in protectedRoutes. Zonder die regel was /issues publiek
bereikbaar geweest — de pagina leunt op de sessie voor de accessible-set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
De drie secties — registratie, onderzoek, oplossing — bewerken inline in
plaats van via een edit-dialog. Het zijn lange markdown-velden, en je wilt ze
schrijven mét het logboek, de koppelingen en de spiegelstatus in beeld; een
dialog neemt precies die context weg.

Sluiten heeft wél een eigen dialoog, want daar hoort een verplichte keuze bij:
een gesloten issue zonder afhandeling bestaat niet. De statuskeuze toont
alleen transities die mogen, zodat de UI niet iets aanbiedt wat de server
vervolgens met 422 weigert.

"Sync nu" wacht wél op de uitkomst en toont die — anders dan de
achtergrondsync na een mutatie. De backoff is een sweep-schedulingregel; een
handmatige poging mag direct.

Het entity-profile staat in docs/patterns/dialogs/issue.md, inclusief de
motivatie voor het inline-bewerken en wat bewust buiten v1 blijft.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs(issues): rollout-runbook, envs en agent-adoptie-snippet
Some checks failed
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 3m44s
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
2ae936f044
Het runbook scheidt wat de code doet van wat JP zelf moet doen. De
outward-facing stappen — SYSTEM-producten aanmaken, de infra-repo, en vooral
de agent-users als ProductMember zetten — staan er niet voor de volledigheid:
zonder die laatste weigert userCanAccessProduct élke create_issue, en dan
faalt de E2E-gate op autorisatie in plaats van op het gedrag dat je test.
Vandaar dat het een expliciete preconditie is.

De agent-adoptie-snippet is onderdeel van de rollout, geen bijlage. Zonder een
regel in de guide komt er nooit een issue binnen en blijft de tracker leeg.

ISSUE_FORGEJO_SYNC is bewust een losse string en geen enum: de noodrem
vergelijkt op 'off', en een per ongeluk lege waarde mag niet de hele
env-validatie laten omvallen. SCRUM4ME_BASE_URL haalt zijn default uit de
shared render-module, zodat de URL bij een domeinverhuizing niet op drie
plekken uiteenloopt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer requested changes 2026-08-17 08:06:12 +02:00
Dismissed
s4m-codex-reviewer left a comment

Verdict: REQUEST_CHANGES

geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • errorprisma/schema.prisma:1537 — De PR voegt HubPermissionRuleScope, HubPermissionRuleList, HubPermissionRuleOrigin, HubPermissionRuleStatus en model HubPermissionRule toe aan het Prisma schema, maar de nieuwe migratie prisma/migrations/20260817090000_issue_tracker/migration.sql maakt alleen de issue-tracker types/tabellen aan. Daardoor is het Prisma schema niet meer conform de migratiegeschiedenis: een verse database die deze PR migreert mist de hub_permission_rules-tabel en bijbehorende enum-types, terwijl Prisma Client ze wel verwacht. Verplaats deze unrelated schema-wijziging naar de migratie die hem introduceert of voeg de ontbrekende migratie expliciet toe in deze PR.

Verdict-log

REQUEST_CHANGES vanwege schema/migratie-drift. De issue-tracker bevat tests en documentatie, maar deze drift is blokkerend voor betrouwbare deploys en lokale setup.

# Verdict: REQUEST_CHANGES geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - **error** — `prisma/schema.prisma:1537` — De PR voegt `HubPermissionRuleScope`, `HubPermissionRuleList`, `HubPermissionRuleOrigin`, `HubPermissionRuleStatus` en `model HubPermissionRule` toe aan het Prisma schema, maar de nieuwe migratie `prisma/migrations/20260817090000_issue_tracker/migration.sql` maakt alleen de issue-tracker types/tabellen aan. Daardoor is het Prisma schema niet meer conform de migratiegeschiedenis: een verse database die deze PR migreert mist de `hub_permission_rules`-tabel en bijbehorende enum-types, terwijl Prisma Client ze wel verwacht. Verplaats deze unrelated schema-wijziging naar de migratie die hem introduceert of voeg de ontbrekende migratie expliciet toe in deze PR. ## Verdict-log REQUEST_CHANGES vanwege schema/migratie-drift. De issue-tracker bevat tests en documentatie, maar deze drift is blokkerend voor betrouwbare deploys en lokale setup.
fix(issues): ontbrekende hub_permission_rules-migratie + ideeën-select user-scoped
Some checks failed
CI / Lint, Typecheck, Test & Build (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
9bf431682d
Twee bevindingen: één van de PR-reviewer, één uit de security-review.

**Schema/migratie-drift (s4m-codex-reviewer).** De submodule-bump naar de
issue-tracker-basis nam ook HubPermissionRule mee — dat model landde in
scrum4me-shared #50, vóór de issue-PR. Het consumer-schema is gegenereerd en
dus alles-of-niets, maar de bijbehorende migratie was er nooit gekomen: een
verse `prisma migrate deploy` bouwde een database zonder hub_permission_rules
terwijl de gegenereerde client hem wél verwacht. De migratie draagt de twee
invarianten die Prisma's DSL niet kan uitdrukken en die het shared-commentaar
expliciet aan de consumer opdraagt: de partial unique index die GLOBAL-rijen
dedupt (de composite UNIQUE kan dat niet, want Postgres ziet NULLs als
onderling verschillend) en de CHECK die scope aan repo_key bindt. De tabel
blijft leeg — de feature zelf is nog niet gebouwd.

**Ideeën lekten over de gebruikersgrens.** De link-select op de
issue-detailpagina haalde alle ideeën van het product op, zonder user_id. Elke
andere idea-query in de app filtert daarop, en niet toevallig: issues zijn
bewust team-zichtbaar, ideeën bewust niet. Zonder die filter zag ieder
productlid codes en titels van andermans ideeën, en kon hij ze koppelen —
waarna de code ook in de gespiegelde Forgejo-body belandde.

Beide bevindingen krijgen een vormtest, zodat ze niet stil terugkomen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

Finding verwerkt in 9bf4316 — de review had gelijk.

Geverifieerd voordat ik iets aanpaste: git show main:prisma/schema.prisma | grep -c HubPermissionRule geeft 0, de branch geeft 10, en er bestond geen migratie met hub_permission_rules. De drift is dus echt en komt van deze PR.

Oorzaak: het consumer-schema is gegenereerd uit vendor/scrum4me-shared en daarmee alles-of-niets. De submodule-bump die de issue-tracker-modellen ophaalt (54277fec63f0bd) nam ook HubPermissionRule mee, dat in scrum4me-shared #50 landde vóór de issue-PR. Scrum4Me had die consumer-migratie nooit gekregen.

Toegevoegd: prisma/migrations/20260815090000_hub_permission_rules/ — die naam en dat tijdstempel staan zo in het shared-model-commentaar genoemd. De migratie draagt de twee invarianten die Prisma's schema-DSL niet kan uitdrukken en die het commentaar expliciet aan de consumer opdraagt:

  • de partial unique index (scope, rule, list) WHERE repo_key IS NULL — de composite @@unique dedupt alleen PRODUCT-rijen, want Postgres beschouwt NULLs als onderling verschillend;
  • de CHECK ((scope = 'GLOBAL') = (repo_key IS NULL)), die scope aan repo_key bindt.

De tabel blijft leeg: de approval-rules-sync-feature zelf is nog niet gebouwd. Ordening klopt — 20260815… komt vóór 20260817_issue_tracker, en geen van beide is op de deploys toegepast, dus migrate deploy blijft chronologisch.

Vormtest __tests__/db/hub-permission-rules-migration.test.ts legt beide invarianten vast.


In dezelfde commit zit ook een bevinding uit de aparte security-review van spec §10: de ideeën-select op de issue-detailpagina filterde niet op user_id. Issues zijn bewust team-zichtbaar, ideeën bewust niet — élke andere idea-query in de app scopet op gebruiker. Zonder die filter zag ieder productlid codes en titels van andermans ideeën. Ook die krijgt een regressietest.

npm run verify groen: 266 testbestanden, 2114 tests.

**Finding verwerkt in `9bf4316` — de review had gelijk.** Geverifieerd voordat ik iets aanpaste: `git show main:prisma/schema.prisma | grep -c HubPermissionRule` geeft 0, de branch geeft 10, en er bestond geen migratie met `hub_permission_rules`. De drift is dus echt en komt van deze PR. Oorzaak: het consumer-schema is gegenereerd uit `vendor/scrum4me-shared` en daarmee alles-of-niets. De submodule-bump die de issue-tracker-modellen ophaalt (`54277fe` → `c63f0bd`) nam ook `HubPermissionRule` mee, dat in scrum4me-shared #50 landde vóór de issue-PR. Scrum4Me had die consumer-migratie nooit gekregen. Toegevoegd: `prisma/migrations/20260815090000_hub_permission_rules/` — die naam en dat tijdstempel staan zo in het shared-model-commentaar genoemd. De migratie draagt de twee invarianten die Prisma's schema-DSL niet kan uitdrukken en die het commentaar expliciet aan de consumer opdraagt: - de **partial unique index** `(scope, rule, list) WHERE repo_key IS NULL` — de composite `@@unique` dedupt alleen PRODUCT-rijen, want Postgres beschouwt NULLs als onderling verschillend; - de **CHECK** `((scope = 'GLOBAL') = (repo_key IS NULL))`, die scope aan repo_key bindt. De tabel blijft leeg: de approval-rules-sync-feature zelf is nog niet gebouwd. Ordening klopt — `20260815…` komt vóór `20260817_issue_tracker`, en geen van beide is op de deploys toegepast, dus `migrate deploy` blijft chronologisch. Vormtest `__tests__/db/hub-permission-rules-migration.test.ts` legt beide invarianten vast. --- In dezelfde commit zit ook een bevinding uit de aparte security-review van spec §10: de ideeën-select op de issue-detailpagina filterde niet op `user_id`. Issues zijn bewust team-zichtbaar, ideeën bewust niet — élke andere idea-query in de app scopet op gebruiker. Zonder die filter zag ieder productlid codes en titels van andermans ideeën. Ook die krijgt een regressietest. `npm run verify` groen: 266 testbestanden, 2114 tests.
docs(issues): rollout-runbook noemt beide migraties
Some checks failed
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 3m33s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
56d9edb815
De hub_permission_rules-migratie komt onvermijdelijk mee met de submodule-bump
en moet dus in de rollout-checklist staan, met de reden erbij — anders lijkt
het een vergissing die iemand terecht zou terugdraaien. Plus de controle die
je vooraf wilt doen als er ergens tóch al zo'n tabel draait.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer left a comment

Verdict: APPROVED

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • Geen blocker/error-severity findings gevonden.

Review-notities

  • De nieuwe server actions volgen het vastgelegde patroon: auth/demo-guard, rate-limit, Zod-validatie, product-scoping met productAccessFilter, transactionele mutaties en revalidatePath.
  • De route-handler/SSE en cron-endpoint zijn bewust gescheiden: cookie-auth voor realtime UI, bearer/CRON_SECRET voor cron. Dat wijkt niet problematisch af van de gelezen route-handlerdoc omdat dit geen publieke REST-consumer endpoint is.
  • De Forgejo mirror-executor dekt de belangrijkste correctheidsrisico’s af met post-lock read, advisory lock, forgejo_sync_seq CAS, failure-registratie en tests voor stale/failure/lock-paden.
  • Datamodel en migraties zijn groot maar additief, met relevante checks/indexen en tests voor migratievorm. Docs/runbook zijn meegeleverd.
# Verdict: APPROVED Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - Geen blocker/error-severity findings gevonden. ## Review-notities - De nieuwe server actions volgen het vastgelegde patroon: auth/demo-guard, rate-limit, Zod-validatie, product-scoping met `productAccessFilter`, transactionele mutaties en `revalidatePath`. - De route-handler/SSE en cron-endpoint zijn bewust gescheiden: cookie-auth voor realtime UI, bearer/`CRON_SECRET` voor cron. Dat wijkt niet problematisch af van de gelezen route-handlerdoc omdat dit geen publieke REST-consumer endpoint is. - De Forgejo mirror-executor dekt de belangrijkste correctheidsrisico’s af met post-lock read, advisory lock, `forgejo_sync_seq` CAS, failure-registratie en tests voor stale/failure/lock-paden. - Datamodel en migraties zijn groot maar additief, met relevante checks/indexen en tests voor migratievorm. Docs/runbook zijn meegeleverd.
Sign in to join this conversation.
No reviewers
No labels
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!174
No description provided.