cmap functions, email, architecture documentation.

This commit is contained in:
2026-08-18 03:33:10 +02:00
parent 12f1ed2764
commit 63b7ca0853
33 changed files with 3974 additions and 80 deletions
+76
View File
@@ -0,0 +1,76 @@
# Analyseerbaarheid en diagnose
Analyseerbaarheid is het vermogen om van een symptoom naar de verantwoordelijke laag en toestand te komen. racket-wiki heeft daarvoor een gunstige eenvoudige processtructuur, maar nog geen volledige observabilitylaag.
## Begin bij de grens
Classificeer een probleem eerst op basis van het eerste aantoonbaar onjuiste resultaat:
| Waarneming | Eerste controle |
| --- | --- |
| Server start of compileert niet | Racket-fout, ontbrekende `require`, migratie en configuratie |
| Request geeft 4xx/5xx | Route, sessie/rol/CSRF, requestbody en serverexception |
| Database-inhoud is onjuist | Opslagprocedure, basisversie en transactiestappen |
| API-JSON klopt maar scherm niet | `wiki.js`-state, route, DOM-id en renderfunctie |
| Alleen CMap-interactie faalt | hit-test, selectie, editorrecord, document vóór/na de handeling |
| Alleen Markdownweergave faalt | expansiestappen, Marked/EasyMDE, DOMPurify en hydratatie |
| Mail zonder STARTTLS werkt wel | TLS-context, certificaatketen, hostnaam en SMTP-capabilities |
Deze aanpak voorkomt dat opslagcode wordt aangepast om een browserregressie te maskeren, of andersom.
## Beschikbare signalen
De backend schrijft start- en setupinformatie en laat technische uitzonderingen in de serverconsole zichtbaar. De frontend schrijft gerichte CMap-diagnostiek met een component- en versieprefx. De browser Network-tab toont requestmethode, status en JSON-response. PostgreSQL kan actuele rijen, versies en constraints rechtstreeks laten zien.
De applicatie heeft daarnaast functionele diagnosebronnen:
- pagina- en CMaphistorie toont welke actor op welk moment een versie schreef;
- `action` en `summary` onderscheiden create, edit, rename, autosave, snapshot en import;
- aliases verklaren waarom een oud pagina-adres nog opent;
- bijlagebeheer toont huidige en historische referenties;
- `/api/ping` onderscheidt browserconnectiviteit van een volledig vastgelopen UI;
- SMTP-testmail is synchroon en geeft de concrete verbindings- of authenticatiefout terug.
## Reproduceerbare diagnose
Leg voor een regressie ten minste vast:
1. softwareversie en databaseschemaversie;
2. rol en route;
3. minimale handelingen vanaf een bekende toestand;
4. verwachte en werkelijke toestand;
5. relevante request/response;
6. actuele en voorgaande versie van het betrokken document;
7. console-uitvoer zonder geheimen.
Voor een CMap-probleem is een klein document met twee of drie items beter dan een productiemap met honderden nodes. Voor TLS is een zelfstandige STARTTLS-spike beter dan meteen de volledige herstelmailketen te wijzigen.
## Logregels
Een diagnostische melding bevat component, softwareversie, bewerking en veilige identifiers zoals pagina- of CMapslug. Log nooit wachtwoorden, ruwe sessietokens, CSRF-tokens, herstelcodes, SMTP-wachtwoorden of volledige databaseconfiguratie.
Gebruik consistente niveaus:
- `info` voor start, migratie, import en afgeronde beheeracties;
- `warning` voor herstelbare afwijkingen, ontbrekende optionele configuratie en overgeslagen importitems;
- `error` voor een mislukte bewerking met voldoende technische oorzaak;
- uitvoerige CMap-events alleen achter een gerichte debugschakelaar wanneer het volume toeneemt.
## Bekende blinde vlekken
Er is momenteel geen request-id die browser, handler en SQL-bewerking verbindt. Er zijn geen structurele timings per endpoint, geen connectionpoolstatistieken en geen centrale foutregistratie. De frontend-CMapdiagnostiek is bruikbaar maar relatief uitvoerig en niet voor alle subsystemen gelijkvormig.
Voeg observability incrementeel toe:
1. eerst een request-id en gestandaardiseerde foutregel;
2. daarna tijdmetingen rond trage endpoints;
3. vervolgens database- en graphmetingen wanneer echte belasting dat vereist;
4. pas dan een externe metrics- of tracingstack wanneer lokaal loggen onvoldoende blijkt.
## Analyseerbare code
Expliciete tussenwaarden, herkenbare domeinfouten en kleine contracten zijn diagnosehulpmiddelen. Een exception `version-conflict` zegt meer dan een generieke SQL-fout. Een procedure die zelf de transactiegrens bevat, maakt duidelijk of een halve opslag mogelijk is.
Gebruik geen brede `with-handlers` die een technische fout omzet in `#f` zonder context. Vang alleen een fout die op die laag betekenisvol vertaald kan worden; laat de oorspronkelijke exception anders door.
Zie [Onderhoudbaarheid](racket-wiki:onderhoudbaarheid) voor de wijzigingswerkwijze en [Testbaarheid](racket-wiki:testbaarheid) voor het omzetten van een diagnose in een regressietest.
+52
View File
@@ -0,0 +1,52 @@
# Architectuur van racket-wiki
Deze documentatieset beschrijft de architectuur van racket-wiki zoals die in versie 0.2.94 bestaat. Zij is tegelijk een wegwijzer voor onderhoud: iedere pagina maakt onderscheid tussen bestaand gedrag, bekende grenzen en regels voor verdere ontwikkeling.
{{cmap:racket-wiki-architectuur}}
## Architectuur in één alinea
racket-wiki is een kleine, zelf gehoste webapplicatie. Eén Racket-proces levert de HTTP-server, authenticatie, autorisatie, setup, databaseaanroepen en statische bestanden. PostgreSQL bevat de duurzame toestand: gebruikers, sessies, pagina's, onveranderlijke paginaversies, conceptmaps, CMap-versies, bijlagen en beheerinstellingen. De browser bevat de interactieve applicatie, Markdown-editor en CMap-editor. De grens tussen browser en backend is een JSON/HTTP-API. Normaal gebruik vereist geen externe CDN of afzonderlijke applicatieservice.
## Leeswijzer
| Onderwerp | Vraag die de pagina beantwoordt |
| --- | --- |
| [Structuur en samenhang](racket-wiki:structuur-en-samenhang) | Welke onderdelen zijn er en hoe zijn zij verbonden? |
| [Gedrag en hoofdscenario's](racket-wiki:gedrag) | Wat gebeurt er bij starten, lezen, wijzigen en herstellen? |
| [Data, transacties en versiebeheer](racket-wiki:data-en-versies) | Welke toestand wordt waar bewaard en welke invarianten gelden? |
| [Modulariteit en afhankelijkheden](racket-wiki:modulariteit) | Waar liggen de modulegrenzen en waar zitten risico's? |
| [Onderhoudbaarheid](racket-wiki:onderhoudbaarheid) | Hoe blijft de code begrijpelijk en wijzigbaar? |
| [Analyseerbaarheid en diagnose](racket-wiki:analyseerbaarheid) | Hoe wordt een storing of regressie gelokaliseerd? |
| [Testbaarheid en teststrategie](racket-wiki:testbaarheid) | Welke tests passen bij welke laag? |
| [Verwachte performance](racket-wiki:performance) | Waar schaalt het ontwerp goed en waar ontstaan knelpunten? |
| [Beveiliging en vertrouwen](racket-wiki:beveiliging) | Welke vertrouwensgrenzen en beveiligingsmaatregelen bestaan? |
| [Regels bij het coderen](racket-wiki:code-regels) | Welke concrete ontwerp- en stijlregels gelden voor nieuwe code? |
| [Beheer en evolutie](racket-wiki:beheer-en-evolutie) | Hoe worden schema, frontend-assets en architectuur gewijzigd? |
{{cmap:racket-wiki-kwaliteitskenmerken}}
## Belangrijkste architectuurbeslissingen
De huidige vorm rust op een klein aantal bewuste keuzes:
1. PostgreSQL is de enige duurzame inhoudsopslag. Ook bijlagen staan als `BYTEA` in de database.
2. Huidige pagina's en CMaps zijn snel leesbare projecties; iedere opslag maakt daarnaast een volledige, onveranderlijke versie.
3. Schrijven gebeurt met optimistic locking. De browser moet het bekende versienummer meesturen.
4. De backend bepaalt authenticatie, rollen, CSRF-controle en database-invarianten. De browser is niet de beveiligingsgrens.
5. Markdown wordt in de browser gerenderd en daarna met DOMPurify gesaneerd.
6. Browserbibliotheken worden tijdens setup vastgepind en lokaal geserveerd.
7. Pagina-adressen bestaan uit een namespace en stabiele slug. Deze set gebruikt de namespace `racket-wiki`.
8. Conceptmaps blijven native CMap-documenten. `{{cmap:...}}` sluit een read-only weergave in Markdown in zonder het CMap-formaat tot Mermaid te reduceren.
## Kwaliteitsbeeld
De architectuur past goed bij een persoonlijke of teamwiki: weinig processen, een duidelijke databasebron en volledige historie. De sterkste punten zijn de transactionele inhoudsopslag, eenvoudige deployment en lokale frontend-assets. De voornaamste ontwikkelpunten zijn de omvang van `server.rkt` en `static/js/wiki.js`, het ontbreken van een volwaardige geautomatiseerde testsuite, een nieuwe databaseverbinding per opslagbewerking en de N+1-aanpak waarmee de volledige navigatiegraaf wordt opgebouwd.
Deze punten zijn geen reden voor een voorafgaande grote herbouw. De ontwikkelregel is: meet eerst, isoleer het concrete probleem en splits een module wanneer een wijziging daar aantoonbaar eenvoudiger of beter testbaar door wordt.
## Eigenaarschap van deze documentatieset
De bestanden onder `architecture/` zijn de bron van deze set. De importmodule plaatst een bronmarkering in iedere geïmporteerde pagina en CMap. Een herimport werkt alleen automatisch bij als de geïmporteerde inhoud sindsdien niet handmatig is gewijzigd. Lokale wijzigingen worden standaard behouden en gerapporteerd.
Gebruik [Beheer en evolutie](racket-wiki:beheer-en-evolutie) voor de importprocedure en de regels om deze documentatie gelijk te laten lopen met de code.
+99
View File
@@ -0,0 +1,99 @@
# Beheer, wijzigingen en architectuurevolutie
## Deze documentatieset importeren
Start de import vanuit de pakketmap met dezelfde datamap als de wiki:
```text
racket architecture/import.rkt --data ./wiki-data --author "Hans Dijkema"
```
Voer eerst een controle zonder inhoudswijzigingen uit:
```text
racket architecture/import.rkt --data ./wiki-data --dry-run
```
De import valideert alle bronbestanden, interne links, CMap-embeds, node-id's en connector-endpoints voordat hij wiki-inhoud schrijft. Daarna maakt hij de twaalf pagina's onder namespace `racket-wiki` en twee CMaps aan of werkt hij ze bij.
## Bescherming van lokale wijzigingen
Iedere geïmporteerde pagina en CMap bevat een bronhash. Bij herimport wordt de actuele inhoud opnieuw gehasht.
- Ongewijzigde geïmporteerde inhoud kan veilig naar de nieuwe pakketversie worden bijgewerkt.
- Inhoud die al exact overeenkomt, krijgt geen zinloze extra versie.
- Handmatig gewijzigde inhoud wordt als `skipped-modified` gemeld en blijft staan.
- Alleen `--overwrite-modified` vervangt bewust zo'n lokale wijziging.
Gebruik overschrijven pas na vergelijking met de pagina- of CMaphistorie:
```text
racket architecture/import.rkt --data ./wiki-data \
--author "Hans Dijkema" --overwrite-modified
```
De importmodule gebruikt de normale procedures uit `storage.rkt` en `cmap-storage.rkt`. Daardoor ontstaan gewone onveranderlijke versies en blijven optimistic locking en afgeleide pagina-indices actief. De import schrijft geen eigen SQL buiten die opslaglaag.
## Bron aanpassen
De bron staat onder `architecture/`:
```text
architecture/
manifest.rktd
import.rkt
pages/
cmaps/
```
Voeg een pagina of CMap eerst aan `manifest.rktd` toe en maak daarna het genoemde bestand. Alle architectuurpagina's gebruiken expliciete Markdown-links naar `racket-wiki:...`. Een ingesloten kaart gebruikt `{{cmap:racket-wiki-...}}` op een eigen regel.
Voer na een wijziging uit:
```text
raco test architecture/import.rkt
racket architecture/import.rkt --data ./wiki-data --dry-run
```
## Releasebeheer
`info.rkt` is de primaire softwareversie. Frontenddiagnostiek bevat dezelfde versie om een browserfout aan het geleverde component te kunnen koppelen. De README houdt een beknopte chronologische changelog bij.
Een releasepakket bevat broncode, statische basisbestanden, Scribble-documentatie en architectuurbronnen. Het bevat niet:
- `wiki-data` of `database.rktd`;
- lokale vendor-downloads uit de datamap;
- gecompileerde output;
- editorback-ups;
- wachtwoorden, tokens of productiedata.
## Database-evolutie
Voor iedere schemawijziging komt een nieuwe migratie aan het einde van de keten. Test zowel een lege database als een upgrade vanaf de vorige release. Een migratie moet de applicatie-invarianten herstellen voordat zij haar versienummer registreert.
Maak vóór een niet-triviale productiemigratie een PostgreSQL-back-up en noteer de herstelprocedure. Omdat inhoud, historie en bijlagen in PostgreSQL staan, is alleen een kopie van de pakketmap geen inhoudsback-up.
## Frontend-evolutie
Vendorbibliotheken zijn vastgepind. Een upgrade van EasyMDE, DOMPurify, highlight.js, diff2html, Lucide of de CMapbasis is een functionele wijziging en vereist gerichte smokechecks. Controleer vooral Markdownpariteit tussen preview, leesweergave en historie, en CMapselectie/hit-testing na een grafische update.
Wanneer browsercode in ES-modules wordt gesplitst, moeten setup, statische routes, cachegedrag en versiecontrole als één wijziging worden behandeld.
## Architectuurbesluiten
Leg een besluit op de relevante architectuurpagina vast wanneer het één van deze zaken verandert:
- proces- of deploymentstructuur;
- modulegrens of afhankelijkheidsrichting;
- database-invariant of versieformaat;
- vertrouwensgrens of beveiligingsstandaard;
- teststrategie;
- schaalverwachting of bewaarbeleid.
Een afzonderlijk zwaar ADR-systeem is nu niet nodig. Een compacte sectie met context, keuze, gevolgen en eventuele terugweg op de betrokken pagina is voldoende.
## Periodieke controle
Controleer bij enkele releases of de documentatie nog overeenkomt met modulelijst, tabellen, routes en actuele knelpunten. Verwijder achterhaalde risico's wanneer zij aantoonbaar zijn opgelost en voeg geen toekomstige componenten als huidige architectuur toe.
De overzichtspagina [Architectuur van racket-wiki](racket-wiki:architectuur) blijft het ingangspunt. [Onderhoudbaarheid](racket-wiki:onderhoudbaarheid) bepaalt wanneer documentatie bij een codewijziging hoort; [Verwachte performance](racket-wiki:performance) bepaalt dat schaalmaatregelen op metingen moeten volgen.
+60
View File
@@ -0,0 +1,60 @@
# Beveiliging en vertrouwen
## Vertrouwensgrenzen
De browser, requestparameters, Markdown, CMap-JSON, bestandsnamen en SMTP-serverreacties zijn onbetrouwbare invoer. De Racket-backend bewaakt identiteit, rol, CSRF en domeinvalidatie. PostgreSQL bewaakt relationele constraints en transacties. Een reverse proxy bewaakt in een productieopstelling doorgaans de externe TLS-verbinding.
Een verborgen knop is geen autorisatie. Een door DOMPurify gesaneerde preview is geen reden om raw HTML elders ongesaneerd in te voegen. Een client-side versienummer is geen waarheid totdat de backend het onder rijlocking met PostgreSQL heeft vergeleken.
## Authenticatie en sessies
Wachtwoorden worden met PBKDF2-HMAC-SHA256 en een hoge iteratiewaarde gehasht. Sessietokens zijn willekeurig; alleen hun SHA-256-hash wordt opgeslagen. Een sessie bevat daarnaast een apart CSRF-token en vervaltijd. Uitgeschakelde gebruikers en verlopen sessies worden niet geaccepteerd.
De sessiecookie moet `Secure` zijn wanneer de externe site HTTPS gebruikt. Cookiebeleid en reverse-proxyheaders moeten bij deployment samen worden getest.
## Rollen
De rangorde is `reader < editor < admin`. Iedere beschermde handler gebruikt server-side rolcontrole. Schrijvende handlers vereisen bovendien CSRF. Nieuwe endpoints krijgen een expliciete minimale rol; zij erven niet toevallig veiligheid doordat de frontend de route niet toont.
## Inhoud en rendering
Markdown wordt door EasyMDE/Marked gerenderd en daarna door DOMPurify gesaneerd. Interne WikiWords, namespaced links, todo-markeringen en CMap-embeds worden via gecontroleerde transformaties toegevoegd. Attributen die na sanering programmatisch worden gemaakt, krijgen alleen gevalideerde slugs en lokaal opgebouwde routes.
Uploads krijgen een server-side veilige opgeslagen naam en MIME-type op basis van bekende extensies. Downloadroutes verwerpen padseparators. Bij uitbreiding met inline actieve formaten zoals SVG of HTML moet afzonderlijk worden beoordeeld of download, sandboxing of sanering nodig is.
## Database en geheimen
SQL gebruikt parameters voor inhoudelijke waarden. Databasecredentials staan in `wiki-data/database.rktd`; de code probeert de bestandsrechten te beperken. De datamap is daarmee geheim en hoort niet in een ZIP, publieke repository of statische webroot.
Ook SMTP-wachtwoorden kunnen in `wiki_settings` of omgevingsvariabelen staan. Zij mogen niet via het beheer-API worden teruggegeven en niet in logregels verschijnen. Een lege wachtwoordinvoer bij de testmail betekent: gebruik het reeds opgeslagen geheim.
## Wachtwoordherstel
De publieke aanvraag meldt niet of gebruiker of e-mailadres bestaat. Herstelcodes zijn willekeurig, alleen gehasht opgeslagen, één uur geldig en eenmalig. Rate limiting begrenst aanvragen per gebruiker. Een succesvolle reset trekt bestaande sessies in.
De publieke basis-URL voor e-mail moet expliciet worden ingesteld; een door de request aangeleverde Host-header is geen betrouwbare basis voor beveiligingslinks.
## SMTP en STARTTLS
Met STARTTLS en **Onvertrouwde certificaten accepteren** uit gebruikt de wiki `ssl-secure-client-context`: certificaatketen en SMTP-hostnaam worden gecontroleerd. Met het vinkje aan gebruikt de wiki `ssl-make-client-context 'auto`: verkeer is versleuteld, maar de serveridentiteit is niet bewezen.
De uitzonderingsoptie is uitsluitend bedoeld voor een bewust vertrouwde lokale mailserver. Een eigen lokale CA toevoegen aan de trust store is veiliger dan verificatie uitschakelen.
## Beschikbaarheid en misbruik
Er gelden al document- en veldvalidaties, maar een algemeen uploadmaximum en request-rate limiting zijn toekomstige versterkingen. De CMap-opslag begrenst het JSON-document tot 10 MiB. Grote Markdown, bijlagen, graph-opbouw en dure zoekvragen kunnen anders geheugen of verwerkingstijd gebruiken.
Voor een publiek bereikbare installatie horen reverse-proxylimits, databaseback-ups, logrotatie en monitoring bij het beveiligingsmodel. Beschikbaarheid is ook een beveiligingseigenschap.
## Beveiligingsregels voor wijzigingen
1. Behoud parametrische SQL.
2. Valideer opnieuw aan de servergrens, ook als de browser valideert.
3. Log nooit geheimen of ruwe tokens.
4. Gebruik veilige standaardinstellingen; een uitzondering vereist een expliciete keuze en uitleg.
5. Maak accountenumeratie niet mogelijk via tekst, statuscode of meetbaar afwijkend gedrag waar praktisch.
6. Test iedere nieuwe write-route op sessie, rol en CSRF.
7. Houd vendorversies vastgepind en controleer downloads via HTTPS.
8. Behandel archiveren, herstellen en importeren als mutaties met auditinformatie.
Zie [Data en versies](racket-wiki:data-en-versies) voor opslaginvarianten en [Testbaarheid](racket-wiki:testbaarheid) voor de noodzakelijke securitytests.
+102
View File
@@ -0,0 +1,102 @@
# Regels bij het coderen
Deze regels gelden voor nieuwe code en voor code die inhoudelijk wordt gewijzigd. Bestaande code hoeft niet mechanisch te worden herschreven zonder functionele reden.
## Algemeen ontwerp
1. Los het probleem op in de laag die eigenaar is van de invariant.
2. Houd de oplossing klein en samenhangend; bouw geen framework voor één geval.
3. Gebruik statische `require`-afhankelijkheden. Vermijd `dynamic-require` als middel om moduleontwerp uit te stellen.
4. Geef configuratie en betekenisvolle dependencies expliciet door.
5. Bewaar beveiliging en dataintegriteit server-side.
6. Maak een databasebewerking atomair wanneer haar afgeleide gegevens samen met de bron moeten veranderen.
7. Behoud backward compatibility van database, CMap-document en API tenzij een migratie expliciet is ontworpen.
## Racket-stijl
Kies expliciete, goed leesbare constructies boven compacte combinaties van kleine idiomen. Gebruik benoemde tussenwaarden wanneer zij de reden van een stap duidelijk maken. Gebruik het Unicode-symbool `λ` voor anonieme procedures.
Schrijf `cond`-clausules met blokhaken:
```racket
(cond
[(not page) #f]
[(archived? page) (archive-result page)]
[else (current-result page)])
```
Vermijd constructies waarin `and`, falsy waarden en filtering tegelijk de datastructuur vormen. Schrijf de bedoelde keuze expliciet:
```racket
(filter (λ (value) value)
(list (if include-title? 'title #f)
(if include-tags? 'tags #f)))
```
Gebruik mutatie wanneer die de toestand werkelijk modelleert en lokaal blijft. Verberg een eenvoudige `hash-set!` of `set!` niet achter een abstractie die geen domeinbetekenis toevoegt.
## Procedures en contractcommentaar
Een publieke of niet-triviale procedure beschrijft waar nuttig:
```text
goal : Waarom bestaat deze procedure?
pre : Welke invoer en toestand worden verondersteld?
post : Welke toestand is na succes veranderd?
result : Welke waarde of fout krijgt de aanroeper?
```
Commentaar verklaart beslissingen, grenzen en afwijkend gedrag. Het herhaalt niet regel voor regel de code.
Gebruik betekenisvolle namen zoals `valid-page-reference?`, `read-concept-map` en `replace-current-attachment-references!`. Een predicaat eindigt op `?`; een procedure die duurzame toestand wijzigt meestal op `!`.
## Module-interfaces
Exporteer alleen wat een andere module gebruikt of als publieke API bedoeld is. Een opslagmodule accepteert domeinwaarden en `config`, niet een HTTP-request. Een HTTP-handler bouwt geen SQL.
Maak een helper wanneer:
- dezelfde invariant op meerdere plaatsen identiek moet blijven;
- de naam domeinbetekenis toevoegt;
- een pure, testbare grens ontstaat;
- foutafhandeling daardoor op de juiste laag komt.
Maak geen helper die slechts één aanroep doorgeeft zonder extra contract.
## SQL en transacties
Gebruik queryparameters voor alle waarden. Dynamisch samengestelde SQL is alleen toegestaan voor vaste, door de code gekozen fragmenten zoals een bekende kolomlijst.
Lees bij een optimistic-lock-write de rij met `FOR UPDATE`, controleer de basisversie en schrijf actuele toestand, historie en afgeleide indices binnen dezelfde transactie. Zet een normale domeinfout om in een herkenbare fout zoals `version-conflict`; verberg onverwachte databasefouten niet.
Een migratie krijgt een nieuw oplopend nummer. Wijzig een reeds uitgebrachte migratie niet alsof installaties haar nog niet hebben uitgevoerd.
## JavaScript en DOM
Gebruik de centrale `api()`-functie voor JSON-calls, zodat CSRF en uniforme foutafhandeling behouden blijven. Routeer via de centrale hashnavigatie; wijzig niet alleen de zichtbare view terwijl de URL achterblijft.
Alle HTML uit Markdown gaat door DOMPurify. Gebruik `textContent` voor gewone tekst en bouw elementen expliciet. Als `innerHTML` noodzakelijk is, moet de bron en sanering in dezelfde functie zichtbaar zijn.
Een nieuwe DOM-id wordt tegelijk in `index.html` en de bijbehorende code toegevoegd. Een nieuwe vertaalkey krijgt ten minste Engelse en Nederlandse standaardtekst.
CMap-bewerkingen gebruiken de editor-API en eindigen als één herkenbare Undo-transactie. Verander niet rechtstreeks een grafisch element zonder het bijbehorende editorrecord en de serialisatie bij te werken.
## Fouten en logging
Vang alleen uitzonderingen die op de huidige laag betekenisvol kunnen worden vertaald. Geef bij een technische fout component en veilige identifier mee. Log geen wachtwoord, databasecredential, sessietoken, CSRF-token of herstelcode.
Browsermeldingen zijn begrijpelijk voor de gebruiker. Technische details mogen aanvullend in console of serverlog staan, maar niet in plaats van een concrete uitleg.
## Versie en release
Bij iedere release:
1. verhoog `info.rkt`;
2. werk zichtbare frontenddiagnostiek en componentversies bij;
3. voeg een README-changelogitem toe;
4. pas relevante architectuurpagina's aan;
5. compileer Racket en draai tests;
6. controleer JavaScript, DOM-id's en vertaalkeys;
7. inspecteer het ZIP-archief op volledigheid en geheimen.
Zie [Onderhoudbaarheid](racket-wiki:onderhoudbaarheid) voor de werkwijze en [Modulariteit](racket-wiki:modulariteit) voor de gewenste afhankelijkheidsrichting.
+86
View File
@@ -0,0 +1,86 @@
# Data, transacties en versiebeheer
## PostgreSQL als bron van waarheid
De database bevat zowel de huidige toestand als de auditgeschiedenis. De browsercache, Undo/Redo-stacks en ingesloten CMaps zijn afgeleide of tijdelijke weergaven en mogen nooit als enige bron van inhoud gelden.
| Tabel | Betekenis |
| --- | --- |
| `users` | Identiteit, profiel, rol, status en wachtwoordhash |
| `sessions` | Gehashte sessietokens, CSRF-token en vervaltijd |
| `pages` | Actuele pagina, namespace, slug, tags, versieteller en zoekvector |
| `page_versions` | Volledige onveranderlijke snapshots van titel, Markdown en tags |
| `page_aliases` | Oude adressen die naar dezelfde pagina-id blijven verwijzen |
| `todo_items` | Afgeleide index van huidige `todo(...)`-markeringen |
| `bookmarks` | Gebruikersspecifieke verwijzingen naar pagina-id's |
| `attachments` | Metadata en volledige binaire inhoud van uploads |
| `attachment_references` | Huidige en historische verwijzingen vanuit paginaversies |
| `concept_maps` | Actuele titel, JSONB-document en versieteller per CMap |
| `concept_map_versions` | Volledige onveranderlijke CMap-snapshots |
| `password_reset_tokens` | Gehashte, tijdelijke en eenmalige herstelcodes |
| `wiki_settings` | Beheerinstellingen, momenteel vooral e-mail |
| `wiki_schema` | Geïnstalleerde migratieversie |
## Pagina-identiteit
Een pagina wordt intern door `pages.id` geïdentificeerd. Het externe adres is `namespace:slug`, of alleen `slug` in de rootnamespace. Namespace en slug zijn afzonderlijke kolommen en samen uniek. Titelwijziging verandert de slug niet automatisch. Bij een echte adreswijziging blijft het oude adres als alias naar dezelfde id bestaan.
Deze scheiding voorkomt dat links bij iedere redactionele titelwijziging breken. Code die relaties bewaart, gebruikt waar mogelijk de id; Markdown en CMaps gebruiken het externe, leesbare paginareferentieformaat.
## Schrijftransactie van een pagina
Een paginaopslag is één atomaire transactie:
1. Zoek en vergrendel de actuele pagina met `FOR UPDATE`.
2. Vergelijk `current_version` met de basisversie van de editor.
3. Verhoog de versie en werk de actuele projectie bij.
4. Voeg dezelfde inhoud toe aan `page_versions`.
5. Bouw de todo-index voor deze pagina opnieuw op.
6. Vervang huidige bijlageverwijzingen en leg historische verwijzingen voor de nieuwe versie vast.
7. Commit alles, of niets.
Een CMap-opslag volgt hetzelfde kernpatroon: rij vergrendelen, versienummer vergelijken, actuele JSONB bijwerken en dezelfde volledige toestand aan `concept_map_versions` toevoegen.
## Waarom volledige snapshots
Volledige snapshots zijn eenvoudig te begrijpen, herstellen en vergelijken. Er is geen keten van patches nodig om versie 37 te reconstrueren en een defecte diff kan de historie niet onleesbaar maken. Voor de verwachte wikiomvang is deze eenvoud belangrijker dan maximale opslagcompactheid.
De prijs is lineaire databasegroei met het aantal versies maal de documentgrootte. Vooral autosave van grote CMaps kan daardoor op termijn veel JSONB opslaan. Bewaarbeleid of deduplicatie hoort pas te worden ontworpen nadat echte datagroei is gemeten; zie [Verwachte performance](racket-wiki:performance).
## Afgeleide gegevens
`pages.search_document`, `todo_items` en `attachment_references.current_reference` zijn afgeleide gegevens. Zij moeten in dezelfde transactie als hun bron worden aangepast. Een los herstelcommando mag ze opnieuw kunnen opbouwen, maar gewone reads mogen niet afhankelijk zijn van een toevallig later achtergrondproces.
De gecombineerde navigatiegraaf is momenteel volledig afgeleid in de browser. Zij wordt niet als databasegraaf bewaard.
## Bijlagen
Een upload hoort bij de id van de eigenaarpagina en krijgt een veilige opgeslagen naam. PostgreSQL bewaart zowel metadata als bytes. Markdown verwijst via de pagina en opgeslagen naam. Huidige en historische referenties worden afzonderlijk gevolgd, zodat beheer kan zien of een bestand alleen nog in oude versies voorkomt.
Het huidige downloadpad leest de volledige `BYTEA` in geheugen voordat de response wordt gemaakt. Dit is eenvoudig en correct voor gebruikelijke wiki-afbeeldingen en documenten, maar vraagt begrenzing of streaming wanneer grote bestanden een doel worden.
## Migraties
`private/migrations.rkt` bevat een oplopende keten. Iedere stap moet herhaalbare SQL gebruiken waar dat nodig is, de nieuwe schemaversie pas na succesvolle omzetting registreren en binnen de omvattende transactie blijven. Oude stappen worden niet achteraf inhoudelijk herschreven, omdat bestaande installaties ze al uitgevoerd kunnen hebben.
Een nieuw schema vereist:
1. een nieuwe migratiestap;
2. aanpassing van `migrate-database!`;
3. controles voor een verse database én een upgrade vanaf de vorige versie;
4. documentatie van nieuwe tabellen, kolommen, indexen en herstelgedrag;
5. een back-up- en terugrolnotitie wanneer de omzetting niet triviaal omkeerbaar is.
## Invarianten
De volgende regels mogen niet alleen in de browser staan:
- iedere actuele versie heeft een overeenkomstige onveranderlijke snapshot;
- versienummers nemen per object strikt toe;
- een stale editor overschrijft geen nieuwere toestand;
- e-mailadressen zijn hoofdletterongevoelig uniek wanneer ingevuld;
- sessies en herstelcodes worden alleen gehasht opgeslagen;
- een bijlage die nog vanuit huidige inhoud wordt gebruikt, wordt niet als orphan verwijderd;
- een gearchiveerde pagina of CMap verschijnt niet in normale lees- en lijstoperaties.
Zie [Beveiliging en vertrouwen](racket-wiki:beveiliging) voor geheimen en [Testbaarheid](racket-wiki:testbaarheid) voor de tests die deze invarianten moeten afdekken.
+61
View File
@@ -0,0 +1,61 @@
# Gedrag en hoofdscenario's
Deze pagina beschrijft gedrag als ketens van waarneembare stappen. De backend blijft in iedere keten verantwoordelijk voor autorisatie en duurzame invarianten; browserstatus is slechts tijdelijke presentatietoestand.
## Start en eerste setup
1. `main.rkt` maakt de datamap aan en leest `database.rktd` wanneer die bestaat.
2. Bij bekende database-instellingen voert `initialize-database!` alle nog ontbrekende migraties in volgorde uit en verwijdert het verlopen sessies.
3. `server.rkt` controleert of schema, een ingeschakelde administrator en alle vendor-assets aanwezig zijn.
4. Zolang een voorwaarde ontbreekt, gaan normale requests naar `/setup`.
5. Setup test PostgreSQL, installeert het schema, maakt de eerste administrator en downloadt de vastgepinde frontendbestanden.
6. Na een volledige setup gaat de browser naar `/login`.
Setup is daarmee ook een reparatiepad voor ontbrekende browserassets. Het is geen anonieme route naar wiki-inhoud.
## Aanmelden en een request uitvoeren
Bij succesvolle authenticatie genereert de backend een willekeurig sessietoken en CSRF-token. Alleen de SHA-256-hash van het sessietoken staat in PostgreSQL; het ruwe token gaat in de cookie naar de browser. Iedere API-handler die inhoud schrijft vereist een geldige sessie, voldoende rol en het CSRF-token uit die sessie.
`reader` leest. `editor` erft lezen en mag pagina's en CMaps creëren, wijzigen, archiveren en bestanden uploaden. `admin` erft editorrechten en beheert gebruikers en systeeminstellingen.
## Pagina lezen
1. De browser haalt eerst de paginacatalogus op zonder alle Markdown.
2. Navigatie naar een pagina vraagt de actuele pagina met Markdown op.
3. Namespaced links en WikiWords worden naar interne routes vertaald.
4. EasyMDE/Marked rendert dezelfde Markdown voor leesweergave, preview en historische versies.
5. DOMPurify saneert de HTML voordat zij in het document komt.
6. Fenced code wordt met highlight.js gemarkeerd en `{{cmap:slug}}` wordt daarna met een read-only CMap gehydrateerd.
Een dubbelklik op een ingebedde CMap opent de volledige editor. De ingesloten kaart is niet een tweede opslagvorm.
## Pagina maken of wijzigen
Een nieuwe pagina krijgt een namespace en stabiele slug. Bij opslaan stuurt de browser titel, Markdown, tags en bij een bestaande pagina het bekende `currentVersion` mee.
De opslagmodule vergrendelt de actuele rij binnen een PostgreSQL-transactie. Alleen als de meegestuurde basisversie gelijk is aan de databaseversie worden actuele toestand, nieuwe onveranderlijke versie, todo-index en bijlageverwijzingen samen vastgelegd. Is iemand anders eerder geweest, dan volgt `version-conflict`; de oudere editor mag de nieuwere versie niet stilzwijgend overschrijven.
Hernoemen of verplaatsen behoudt dezelfde pagina-id en maakt een alias voor het oude adres. Archiveren verwijdert de pagina niet fysiek en behoudt historie en bijlagen.
## Conceptmap bewerken
De CMap-editor houdt items, verbindingszinnen, connectors, groepen en sub-CMaps in één document met `schemaVersion: 1`. Bewerkingen gaan eerst door een lokale Undo/Redo-historie. Na een afgeronde handeling plant de browser een autosave; een nieuwe handeling verschuift die timer. Een lopende save kan een volgende save noodzakelijk maken, zodat tussentijdse wijzigingen niet verdwijnen.
Iedere serveropslag controleert opnieuw het basisversienummer en schrijft een volledige CMap-snapshot. Autosave beschermt de werkstand. **Snapshot maken** schrijft ook zonder inhoudsverschil een herkenbaar historisch moment met een omschrijving. Een historische versie laden verandert alleen de editor; pas een volgende opslag maakt daarvan een nieuwe actuele versie.
## Zoeken, Recent en navigatiegraaf
Paginazoeken gebruikt een opgeslagen gewogen `tsvector` en GIN-index. CMap-zoeken verzamelt titel, slug, labels en synopses uit JSONB. Recent voegt de nieuwste pagina- en CMapwijzigingen samen.
De gecombineerde navigatiegraaf leest verwijzingen uit alle huidige pagina's en CMaps. Hij wordt in de browser gecachet totdat een catalogus opnieuw wordt geladen. Dit levert rijke navigatie op, maar is het belangrijkste schaalrisico; zie [Verwachte performance](racket-wiki:performance).
## Wachtwoordherstel en mail
De publieke herstelactie geeft altijd dezelfde reactie, ook als een account niet bestaat. Een echte aanvraag maakt een gehashte, eenmalige code die één uur geldig is en past rate limiting per gebruiker toe. Na succesvol herstel worden bestaande sessies ingetrokken.
SMTP kan STARTTLS gebruiken. Standaard gebruikt mail `ssl-secure-client-context` en controleert certificaatketen en hostnaam. Alleen voor een bewust vertrouwde lokale server kan de administrator onvertrouwde certificaten accepteren; dan gebruikt de verbinding moderne TLS zonder serverauthenticatie. De testmail gebruikt eerst de nog niet opgeslagen formulierwaarden.
## Foutgedrag
Verwachte domeinfouten worden zo dicht mogelijk bij hun grens herkend: ongeldige input als 400, ontbrekende authenticatie als 401, onvoldoende rechten als 403, ontbrekende inhoud als 404 en versieconflict als 409. Technische uitzonderingen moeten voldoende context in de serverconsole behouden zonder wachtwoorden, tokens of databasegeheimen te loggen.
+62
View File
@@ -0,0 +1,62 @@
# Modulariteit en afhankelijkheden
## Huidige modulegrenzen
De backend is functioneel opgesplitst. Configuratie, databaseverbinding, migraties, authenticatie, paginaopslag, CMap-opslag, mail, setup en vendorbeheer hebben ieder een herkenbare module. Deze modules communiceren hoofdzakelijk met gewone Racket-waarden: structs, hashes, lijsten en strings.
De belangrijkste grens is die tussen `server.rkt` en de opslagmodules. `server.rkt` hoort HTTP te begrijpen; `storage.rkt`, `cmap-storage.rkt` en `auth.rkt` horen domeinbewerkingen en database-invarianten te begrijpen. Geen opslagprocedure mag een webrequest nodig hebben.
De frontend heeft een vergelijkbare grens. `cmap-racket-wiki.js` implementeert een editorcomponent met callbacks voor openen, selectie en wijzigingen. `wiki.js` koppelt die callbacks aan routes, API's en dialoogvensters.
## Sterke punten
De volgende onderdelen zijn al goed geïsoleerd:
- `wiki-config` bundelt runtimekeuzes en voorkomt verspreide globale configuratie.
- `call-with-wiki-database` centraliseert openen en sluiten van verbindingen.
- opslagprocedures kapselen SQL en transacties in.
- de CMap-documentvorm is een expliciet JSON-contract met `schemaVersion`.
- Markdownrendering gaat via één frontendfunctie voor lezen, preview en historie.
- vertalingen hebben één effectieve bron voor backend en frontend.
- frontend-vendorbestanden worden door één module beheerd en gecontroleerd.
## Huidige concentraties
`server.rkt` bevat zowel routing als veel requestparsing en handlerlogica. `static/js/wiki.js` bevat vrijwel de hele single-page applicatie. Dat maakt zoeken eenvoudig, maar vergroot de kans dat een wijziging onverwacht een ander view- of routepad raakt.
`private/storage.rkt` combineert pagina's, historie, zoeken, bookmarks, aliases en uploads. Die onderdelen delen pagina-identiteit en transacties, maar hoeven niet onbeperkt samen te groeien.
Deze concentraties zijn technische schuld, geen automatische opdracht tot een grote opsplitsing. Een splitsing moet een concreet voordeel hebben: een kleiner contract, onafhankelijke tests of duidelijker eigenaarschap.
## Gewenste afhankelijkheidsregels
1. Een buitenste laag mag een binnenste dienst kennen; omgekeerd niet. HTTP kent opslag, opslag kent geen HTTP.
2. SQL blijft in database- en opslagmodules. JavaScript bouwt geen SQL-achtige querysemantiek na.
3. Authenticatie en autorisatie blijven server-side. UI-zichtbaarheid is alleen gebruiksgemak.
4. Cross-cutting gedrag krijgt één expliciete helper wanneer er werkelijk meerdere gebruikers zijn. Maak geen wrapper voor één triviale aanroep.
5. Een module exporteert alleen procedures en structs die een andere module werkelijk nodig heeft.
6. Gebruik geen `dynamic-require` om een heldere statische afhankelijkheid te verbergen.
7. Een nieuwe module krijgt één samenhangende reden om te wijzigen; een verzameling toevallige helpers is geen moduleontwerp.
## Waarschijnlijke toekomstige extracties
Wanneer `server.rkt` verder groeit, ligt opsplitsing per adaptergebied voor de hand: sessie/profiel, pagina's, CMaps en beheer. De centrale router kan dan dun blijven. Handlers ontvangen `config` expliciet en roepen dezelfde bestaande diensten aan.
Wanneer `wiki.js` verder groeit, zijn route/state, pagina-editor, CMap-host, graph-view en admin-views natuurlijke grenzen. Splits alleen met native ES-modules wanneer de setup- en cacheversies van alle scripts tegelijk beheerst worden.
Wanneer paginaopslag wordt aangepast, kunnen bookmarks, aliases en uploads later eigen modules krijgen. De pagina-schrijftransactie en afgeleide indices moeten daarbij als één consistente operatie behouden blijven; opsplitsing van bestanden mag geen opsplitsing van de transactie veroorzaken.
## Contracten tussen modules
Een goed contract specificeert:
- geldige invoer en normalisatie;
- welke toestand wordt gelezen of gewijzigd;
- transactiegrens en concurrencygedrag;
- resultaatvorm;
- herkenbare domeinfouten;
- maximale document- of uploadgrootte waar relevant.
De bestaande `goal / pre / post / result`-commentaren zijn hiervoor geschikt, zolang zij het niet-zichtbare contract beschrijven en niet slechts de procedurecode navertellen.
Zie [Regels bij het coderen](racket-wiki:code-regels) voor de concrete stijl en [Testbaarheid](racket-wiki:testbaarheid) voor het testen per grens.
+69
View File
@@ -0,0 +1,69 @@
# Onderhoudbaarheid
Onderhoudbaarheid betekent hier dat een ontwikkelaar een wijziging kan lokaliseren, de gevolgen kan overzien en het resultaat kan controleren zonder eerst de hele applicatie te herschrijven.
## Wat al helpt
De applicatie heeft één codebase, één backendproces en één duurzame database. Versies staan centraal in `info.rkt`; schemawijzigingen hebben een oplopende migratieketen. Pagina- en CMapversies zijn volledig en onveranderlijk, waardoor fouten in de actuele toestand niet meteen de historie vernietigen.
Backendprocedures hebben vaak een contractcommentaar met doel, preconditie, postconditie en resultaat. De browsercode gebruikt vergelijkbare JSDoc-blokken bij ingewikkelde functies. Frontendbibliotheken zijn vastgepind, zodat een CDN-update niet onverwacht productiegedrag verandert.
## Onderhoudsrisico's
De grootste risico's zijn concentratie en impliciete koppeling:
- `server.rkt` en `wiki.js` kennen veel scenario's;
- HTML-id's in `index.html` en `$()`-aanroepen in `wiki.js` vormen een compileertijd-onzichtbaar contract;
- vertaalkeys worden tussen Racket, HTML en JavaScript gedeeld;
- CMap-documentvelden worden zowel door opslag als editorcode verondersteld;
- versiestempels staan behalve in `info.rkt` ook in frontenddiagnostiek;
- de huidige geautomatiseerde testdekking is beperkt tot de nieuwe architectuurimportmodule.
Deze koppelingen moeten bij iedere release statisch of geautomatiseerd worden gecontroleerd.
## Wijzigingswerkwijze
Een onderhoudbare wijziging volgt deze volgorde:
1. Beschrijf eerst de waarneembare regressie of gewenste invariant.
2. Zoek de laag die eigenaar is van die invariant.
3. Maak de kleinste samenhangende wijziging op die laag.
4. Voeg een regressietest toe op het laagst mogelijke niveau.
5. Controleer aangrenzende contracten: database, API, browserstate, DOM-id's en vertalingen.
6. Werk versienummer, changelog en relevante architectuurpagina bij.
7. Voer compileer-, JavaScript-, import- en smokecontroles uit.
Bij een CMap-interactieregressie hoort bijvoorbeeld eerst een editorcomponenttest of gerichte browser-spike. Een aanpassing in opslagcode is alleen passend wanneer het opgeslagen document onjuist is.
## Leesbaarheid als ontwerpkeuze
Nieuwe Racket-code kiest expliciete, leesbare constructies boven compacte combinaties van idiomen. Een afzonderlijke `if`, benoemde tussenwaarde of kleine inhoudelijke helper is beter dan een korte expressie waarvan falsy filtering, mutatie en datastructuurkennis tegelijk begrepen moeten worden. Gebruik `λ` voor anonieme procedures.
Leesbaarheid betekent niet dat iedere aanroep een wrapper krijgt. Een helper verdient een naam wanneer die naam domeinbetekenis toevoegt, herhaling veilig centraliseert of een testbare grens maakt.
Zie [Regels bij het coderen](racket-wiki:code-regels) voor de volledige set.
## Versies en compatibiliteit
Een release verandert `info.rkt` en alle zichtbare frontendversiestempels. Bestaande databases worden uitsluitend via een nieuwe migratie vooruitgebracht. Een CMap-documentwijziging verhoogt `schemaVersion` en vereist expliciete leescompatibiliteit of migratie; onbekende velden mogen niet zonder besluit verdwijnen.
API-contracten worden bij voorkeur compatibel uitgebreid. Wanneer een veld verplicht wordt, moeten oude browserassets en nieuwe backend niet tijdelijk onverenigbaar kunnen worden geserveerd. Cacheversies en deploymentvolgorde horen daarom bij de wijziging.
## Documentatie als onderdeel van de code
Deze architectuurset staat als bronbestanden in het pakket. De importmodule valideert interne paginalinks, CMap-embeds, nodeverwijzingen en connector-endpoints voordat zij iets schrijft. Daardoor is documentatiesamenhang niet alleen een handmatige afspraak.
Een wijziging die een modulegrens, gegevensinvariant, beveiligingsregel, teststrategie of schaalverwachting verandert, is pas afgerond wanneer de bijbehorende pagina is aangepast. Gebruik [Beheer en evolutie](racket-wiki:beheer-en-evolutie) om de set opnieuw te importeren zonder lokale wijzigingen stilzwijgend te overschrijven.
## Vermijd voortijdige herbouw
Onderhoudbaarheid neemt niet automatisch toe door meer lagen, dependency injection of generieke frameworks. Voor racket-wiki blijft de voorkeur:
- directe statische dependencies;
- gewone Racket-waarden;
- kleine publieke interfaces;
- transacties rond echte invarianten;
- extractie na aantoonbare herhaling of testbehoefte;
- meting vóór performance-architectuur.
Dat houdt de applicatie passend bij haar doel: een begrijpelijke, zelf te beheren wiki en geen platform dat zijn eigen uitbreiding belangrijker maakt dan de inhoud.
+80
View File
@@ -0,0 +1,80 @@
# Verwachte performance op lange termijn
## Status van deze verwachting
De onderstaande schaalinschatting is een architectuuranalyse, geen benchmark. Werkelijke grenzen hangen af van PostgreSQL, netwerkvertraging, documentgrootte, aantallen versies, browser en gelijktijdige gebruikers. Optimaliseer pas na meting met representatieve data.
## Verwacht schaalbeeld
| Omvang | Verwachting met huidige architectuur |
| --- | --- |
| Honderden pagina's, tientallen CMaps | Normale lees-, schrijf- en zoekacties horen ruim voldoende te zijn op een gewone lokale server. |
| Enkele duizenden pagina's, honderden CMaps | Paginaweergave en geïndexeerd paginazoeken blijven waarschijnlijk goed; volledige graph-opbouw, CMap-zoeken, catalogusgrootte en verbindingsopbouw worden zichtbaar. |
| Tienduizenden pagina's of veel grote CMaps | Paginering, server-side graphindex, connectionpooling, geïndexeerd CMap-zoeken en versie-/bijlagebeleid worden waarschijnlijk noodzakelijk. |
Het ontwerp is dus passend voor een persoonlijke of teamwiki. Het is niet zonder aanvullende maatregelen ontworpen als internetbrede kennisbank met zeer veel gelijktijdige gebruikers.
## Sterke performance-eigenschappen
De actuele pagina staat direct in `pages`; voor normaal lezen hoeft geen versiegeschiedenis te worden opgebouwd. Titel en Markdown leveren een opgeslagen gewogen zoekvector met GIN-index. PostgreSQL verwerkt transacties en locking dicht bij de data. De browser ontvangt bij de paginacatalogus alleen metadata en haalt Markdown pas op bij gebruik.
Vendor-assets worden lokaal geserveerd en veranderen niet tijdens normaal gebruik. De browser cachet de opgebouwde navigatiegraaf totdat pagina- of CMapcatalogus opnieuw wordt geladen.
## Belangrijkste toekomstige knelpunten
### Volledige navigatiegraaf
`loadGraphData()` haalt momenteel iedere pagina en iedere CMap sequentieel via een eigen API-request op en extraheert daarna links in de browser. Voor `P` pagina's en `C` CMaps zijn dat `P + C` inhoudsrequests naast de catalogi. Latency groeit daardoor lineair en netwerkvertraging telt herhaaldelijk op.
De duurzame oplossing is een server-side relationele linkindex die in dezelfde schrijftransactie als pagina of CMap wordt bijgewerkt. Een graph-endpoint kan dan alle nodes en edges in één compacte response leveren.
### CMap-zoeken
Paginazoeken gebruikt een opgeslagen index. CMap-zoeken bouwt per query tekst uit JSONB-items en maakt daar op dat moment een `tsvector` van. Bij veel of grote kaarten wordt dit een volledige scan. Voeg dan een opgeslagen zoektekst/`tsvector` en GIN-index aan `concept_maps` toe, bijgewerkt tijdens CMap-opslag.
### Databaseverbindingen
`call-with-wiki-database` opent en sluit voor iedere dienstaanroep een PostgreSQL-verbinding. Dit houdt lifecycle en foutisolatie eenvoudig, maar connection setup wordt bij hogere requestfrequentie overhead. Een begrensde connectionpool is dan logischer dan één globale verbinding. Meet eerst connecttijd, querytijd en gelijktijdigheid afzonderlijk.
### Catalogi
`list-pages` en `list-concept-maps` leveren alle huidige metadata. Dat is prettig voor client-side navigatie, WikiWords en comboboxen. Bij tienduizenden objecten worden responsegrootte, browsergeheugen en sorteren merkbaar. Mogelijke vervolgstappen zijn server-side paginering, een compacte referentiecatalogus en lazy loading per namespace.
### Volledige versies
Iedere pagina- en CMapsave bewaart een volledige snapshot. Opslag groeit lineair; CMap-autosave kan veel bijna gelijke JSONB-documenten opleveren. Meet per object het aantal versies, bytes en savefrequentie. Een later beleid kan oude autosaves uitdunnen terwijl expliciete snapshots en betekenisvolle saves blijven bestaan. Zo'n beleid vereist eerst een expliciete bewaargarantie en back-up.
### Bijlagen
Bijlagebytes staan in PostgreSQL en worden volledig in geheugen gelezen voor de response. Stel een maximale uploadgrootte in voordat grote media worden ondersteund. Voor echt grote objecten zijn streaming of een object store mogelijk, maar dat verandert back-up, autorisatie en referentiebeheer en is dus geen kleine optimalisatie.
### Browserrendering
Grote CMaps tekenen veel DOM/SVG-objecten en connectors. Volledige maps en graphs hebben uiteindelijk layoutkosten die niet met een snellere database verdwijnen. Meet nodeaantal, rendertijd en interactieframes. Sub-CMaps, viewportculling of vereenvoudigde read-only rendering zijn dan gerichte opties.
## Meetplan
Voeg vóór optimalisatie minimaal deze metingen toe:
- requestduur per endpoint en responsebytes;
- connecttijd versus SQL-tijd;
- aantallen pagina's, CMaps, graph-edges en versies;
- gemiddelde en p95 Markdown-, JSONB- en bijlagegrootte;
- tijd voor volledige graph-opbouw;
- tijd voor CMap-zoekquery's;
- browserrendertijd bij representatieve CMaps.
Gebruik datasets met echte linkdichtheid en documentgroottes. Duizend lege pagina's voorspellen het gedrag van duizend technische documenten niet.
## Optimalisatievolgorde
De verwachte volgorde bij groei is:
1. graph-edges tijdens opslag indexeren en in één request leveren;
2. CMap-zoekvector opslaan en indexeren;
3. databaseconnectionpool toevoegen;
4. catalogi pagineren of per namespace laden;
5. expliciet versie- en uploadbeleid ontwerpen;
6. pas daarna horizontale of servicegerichte architectuur overwegen.
Deze volgorde behoudt de eenvoud van [Structuur en samenhang](racket-wiki:structuur-en-samenhang) zolang die nog waardevol is.
@@ -0,0 +1,67 @@
# Structuur en samenhang
## Context
racket-wiki bestaat tijdens normaal gebruik uit drie uitvoerende delen:
| Deel | Verantwoordelijkheid |
| --- | --- |
| Browser | Navigatie, lokale UI-toestand, EasyMDE, Markdownweergave, CMap-bewerking en grafische relaties |
| Racket-proces | HTTP-routing, sessies, rollen, CSRF, validatie, opslagorkestratie, setup en statische bestanden |
| PostgreSQL | Duurzame toestand, relationele integriteit, transacties, historie, zoekindexen en binaire bijlagen |
Er is geen afzonderlijke Node-server, Markdownservice of object store. Na setup worden frontendbibliotheken lokaal door hetzelfde Racket-proces geserveerd.
## Bronstructuur
`main.rkt` is het programma- en bibliotheekingangspunt. Het bouwt een `wiki-config`, initialiseert zo nodig de database en start `server.rkt`.
`server.rkt` vormt de HTTP-adapter. Het koppelt URL's en methoden aan handlers, vertaalt requests naar domeinbewerkingen en vertaalt resultaten of fouten naar HTML/JSON-responses. Setup, login en wachtwoordherstel zijn server-rendered; de normale wiki is een single-page browserapplicatie.
De map `private/` bevat de backendonderdelen:
| Module | Hoofdtaak |
| --- | --- |
| `config.rkt` | Runtimeconfiguratie en paden |
| `database.rkt` | PostgreSQL-instellingen, verbinding en initialisatie |
| `migrations.rkt` | Opeenvolgende, transactionele schemasprongen |
| `auth.rkt` | Gebruikers, wachtwoorden, rollen, sessies, CSRF en herstelcodes |
| `storage.rkt` | Pagina's, historie, zoeken, bookmarks, uploads en aliases |
| `cmap-storage.rkt` | Conceptmaps en onveranderlijke CMap-versies |
| `attachment-references.rkt` | Huidige en historische verwijzingen naar bijlagen |
| `todo.rkt` | Herkennen van wiki-brede `todo(...)`-markeringen |
| `mail.rkt` | SMTP-instellingen, STARTTLS, testmail en herstelmail |
| `setup.rkt` | Eerste websetup en reparatiepad |
| `vendor.rkt` | Ophalen en controleren van vastgepinde browserbibliotheken |
| `http-util.rkt` | Gemeenschappelijke response- en requesthulpen |
| `version.rkt` | Softwareversie uit `info.rkt` |
`translate.rkt` staat bewust aan de publieke rand: zowel backend als frontend gebruiken dezelfde effectieve vertaaltabel.
De map `static/` bevat de browserapplicatie. `static/index.html` definieert de views en dialogen. `static/js/wiki.js` beheert routing, API-aanroepen, editor- en paginatoestand en speciale views. `static/cmap/cmap.js` levert de grafische basis; `cmap-racket-wiki.js` voegt wiki-items, selectie, relaties, sub-CMaps, historie en documentserialisatie toe. `combobox.js` is een herbruikbaar klein UI-onderdeel. CSS is verdeeld tussen algemene wiki-opmaak en CMap-opmaak.
## Afhankelijkheidsrichting
De bedoelde richting is:
1. `main.rkt` kent configuratie, database-initialisatie en server.
2. `server.rkt` kent backenddiensten, maar backendopslag kent geen HTTP-requests.
3. Opslagmodules kennen `database.rkt` en dataconversies, maar geen browserdetails.
4. De browser kent alleen HTTP-contracten en de CMap-component; hij kent geen SQL.
5. PostgreSQL kent alleen schema en constraints; het kent geen HTML of routes.
Deze richting houdt de belangrijkste domeinregels buiten de UI. Een rolcontrole die uitsluitend een knop verbergt, is bijvoorbeeld onvoldoende: `server.rkt` moet dezelfde bewerking weigeren.
## Samenhang via hoofdgegevens
Een pagina heeft een database-id, namespace, slug, actuele Markdown en een versieteller. `page_versions` verwijst naar dezelfde pagina-id. Todo's, bookmarks, aliases en bijlageverwijzingen sluiten via die id aan.
Een CMap heeft een stabiele slug, titel, JSONB-document en versieteller. `concept_map_versions` bewaart volledige JSONB-snapshots. CMap-items kunnen via `pageSlug` naar een wikipagina verwijzen en via `cmapSlug` naar een andere CMap. De browser gebruikt deze verwijzingen voor navigatie en de gecombineerde sitegraph.
Zie [Data, transacties en versiebeheer](racket-wiki:data-en-versies) voor de invarianten en [Modulariteit en afhankelijkheden](racket-wiki:modulariteit) voor de gewenste grenzen bij uitbreiding.
## Deploymentstructuur
De installatie bevat code en statische basisbestanden. De configureerbare datamap bevat `database.rktd`, de gekozen taal en lokaal gedownloade vendor-assets. Inhoud en bijlagen staan in PostgreSQL. Daardoor moet een volledige back-up zowel de database als de kleine datamap met configuratie bevatten.
De Racket-server kan rechtstreeks luisteren, maar in productie ligt HTTPS gewoonlijk bij een reverse proxy. `secure-cookie?` moet dan aan staan en de publieke URL voor herstelmail moet naar de externe HTTPS-URL wijzen.
+66
View File
@@ -0,0 +1,66 @@
# Testbaarheid en teststrategie
## Huidige toestand
De backendfuncties zijn meestal procedureel en krijgen `config` expliciet mee. Dat is een goede basis voor tests. PostgreSQL-invarianten zitten echter in concrete opslagprocedures en er is nog geen projectbrede geautomatiseerde testsuite met tijdelijke database.
Versie 0.2.93 introduceert wel tests in `architecture/import.rkt`. Die controleren de manifestset, interne paginalinks, CMap-embeds, unieke node-id's, connector-endpoints en de bronmarkeringen waarmee lokaal gewijzigde importinhoud wordt beschermd. Dit is een begin, geen voldoende dekking van de wiki.
## Testpiramide voor racket-wiki
| Laag | Doel | Voorbeelden |
| --- | --- | --- |
| Pure unit tests | Deterministische omzettingen snel controleren | slugvorming, linkextractie, todoherkenning, markerhashes, TLS-keuze |
| Componenttests | Eén module met echte randvoorwaarden | CMap-document laden/bewaren, Markdowntransformaties, requestparsing |
| PostgreSQL-integratietests | Transacties en constraints bewijzen | create/update/conflict, historie, aliases, bijlagen, migraties |
| HTTP-integratietests | Rollen en API-contracten bewijzen | 401/403/409, CSRF, JSON-vormen, upload/download |
| Browser-smoketests | Kritische gebruikerspaden bewijzen | login, split editor, CMap drag/undo/autosave/snapshot, beheer |
De meeste varianten horen onderin. Een browsertest is te duur om alle slug- of linkgevallen af te dekken; een pure test kan niet bewijzen dat een SQL-transactie werkelijk atomair is.
## Prioriteit voor regressietests
De eerste volwaardige suite moet deze risico's afdekken:
1. twee editors op dezelfde pagina of CMap, waarbij de tweede een 409 krijgt;
2. actuele toestand en onveranderlijke versie worden samen geschreven of samen teruggedraaid;
3. pagina hernoemen behoudt id en maakt een werkende alias;
4. todo- en bijlageindices volgen exact de actuele Markdown;
5. reader, editor en admin kunnen uitsluitend hun toegestane endpoints gebruiken;
6. sessie-, CSRF- en herstelcodes worden correct gevalideerd en ingetrokken;
7. CMap-documenten maken een stabiele load/save-roundtrip;
8. slepen op een sub-CMapframe verplaatst alle afstammelingen, slepen op het hoofdconcept niet;
9. autosave, handmatige save en snapshot krijgen het juiste historische `action` en `summary`;
10. migratie vanaf iedere ondersteunde schemaversie levert dezelfde actuele structuur.
## Tijdelijke PostgreSQL-database
Integratietests gebruiken een afzonderlijke database of tijdelijk schema en nooit de ontwikkel- of productiedatabase. Iedere test begint vanuit een bekende migratiestand en ruimt eigen data op. De testconfiguratie komt uit expliciete testomgevingsvariabelen; wachtwoorden worden niet in de repository opgenomen.
Een praktische eerste stap is één testdatabase per testrun, met unieke namespace of schema per proces. Tests die locking en conflicts controleren gebruiken twee echte verbindingen.
## Testbare grenzen verbeteren
Maak pure omzettingen los van I/O wanneer zij voldoende domeinbetekenis hebben. Voorbeeld: validatie en normalisatie van een paginabewerking kan als pure procedure worden getest; de opslagprocedure gebruikt het resultaat binnen de transactie.
Introduceer geen groot mockframework. Gewone procedures en expliciete parameters zijn voldoende. Wanneer tijd of willekeur een test blokkeert, geef een kleine `current-clock`- of token-generatorparameter door op de laag die deze bron bezit.
Voor frontendcode zijn browsercomponenttests met een echte DOM geschikter dan het namaken van ieder element. De CMap-adapter moet met een klein vast document getest kunnen worden zonder de hele wiki te starten.
## Contract- en structuurcontroles
Naast functionele tests hoort de releasecontrole automatisch te verifiëren:
- alle Racket-modules compileren met `raco make`;
- `raco test` vindt en draait de tests;
- JavaScript kan syntactisch worden geparseerd;
- iedere `$("id")`-referentie heeft een overeenkomstig HTML-element;
- vertaalkeys die de UI gebruikt bestaan;
- alle bestanden in het architectuurmanifest bestaan en verwijzen onderling geldig;
- het ZIP-archief bevat geen `wiki-data`, wachtwoorden, tijdelijke bestanden of buildoutput.
## Wanneer is een wijziging klaar?
Een bugfix is pas klaar wanneer het oorspronkelijke scenario reproduceerbaar is en de test vóór de fix faalt of aantoonbaar het defecte pad raakt. Een nieuw endpoint heeft minimaal opslag-/domeintests en rol-/CSRF-tests. Een schemawijziging heeft zowel verse-installatie- als upgrademigratietests.
Zie [Analyseerbaarheid](racket-wiki:analyseerbaarheid) voor het verzamelen van een minimale reproductie en [Regels bij het coderen](racket-wiki:code-regels) voor testvriendelijke codevormen.