plan.md
raw
· source
Plan — port podman/syntax CMS into tdd.md, SAMA-native
Doel. Het CMS uit ~/Documents/podman (sx-filter + sx-editor + sx-content + Ghost-compat theme) volledig overzetten naar tdd.md, in 100% SAMA-stijl, met de bestaande tdd.md content intact gemigreerd.
Niet-doel. Podman, Caddy, of een tweede service-tier in tdd.md. Alles draait in één Bun-proces dat we al hebben (c11_server.ts). Caddy's rol (TLS + routing) doet onze deploy-laag op p620.
⚠ Eerst beslissen — storage-canon
Dit stuurt elke andere keuze. Twee opties; ik default naar A tenzij je flipt.
A. Git-canon (default — behoudt tdd.md identity)
- Bron-van-waarheid blijft het bare repo
/app/repo(huidige stack). - Elke save in de editor = een commit via bestaande
c14_git.commitFile. - sxdoc-trees (typed blocks) leven als sidecar JSON naast de markdown:
content/blog/foo.md+content/blog/foo.sxdoc.json. - SQLite (bestaande
c13_database) krijgt een afgeleide index-tabel (content_index) voor snelle lijst-queries en taxonomie-lookups, rebuildbaar uit git. Drop het, replaygit log, terug. - Voordeel: "SAMA meets git" verhaal blijft kloppen.
sama-meets-git-cms.mdblijft waarheid. Audit-trail =git log content/. - Nadeel: complexer dan podman's directe SQLite-writes. Trager bij grote sites (>10k posts). Niet relevant op onze schaal.
B. SQLite-canon (1-op-1 podman-port)
content/*.mdwordt eenmalig geïmporteerd naarsx_documents+poststabellen, daarna read-only.- Editor schrijft uitsluitend naar SQLite. Git-history van content stopt op het migratie-commit.
- Voordeel: minimale afwijking van podman's code. Sneller te porten.
- Nadeel: tdd.md verliest "elke content-edit = commit" — kern van het product per memory.
Beslissing 2026-05-11: B (SQLite-canon) + git-als-audit-mirror. Locked.
Locked decisions (2026-05-11)
Storage canon: B (SQLite-canon) + git-als-audit-mirror
- Canoniek:
sx_documentstabel inc13_database(bun:sqlite). Editor reads/writes hier; live-preview en alle render-paden lezen hier. - Audit-mirror: elke save → 1 multi-path commit met
content/{slug}.md(afgeleide markdown-projectie) +content/{slug}.sxdoc.json(canonical JSON-tree). Zo blijftgit log content/de menselijk-leesbare audit-trail; "elke save = een commit" uitsama-meets-git-cms.mdblijft waar — de canoniciteit ligt nu in SQLite, het bewijs in git. - Recovery: SQLite-corruptie? Drop tabel, replay van
*.sxdoc.json. - Initial migration: eenmalig
scripts/migrate_content_to_sxdoc.tsleest huidigecontent/**/*.md→ parseert naarSxDocument→ schrijft SQLite + emit één migratie-commit met alle nieuwe.sxdoc.jsonernaast.
Parser laag: c31 · Render laag: c51
c31_sxdoc_parse.ts(HTML → SxDocument) + siblingc31_sxdoc_parse.test.ts. Reden:content/sama/modeled.mdis expliciet — "every external input has a parser in a c31_ model"*. HTML strings vanuit de editor/migratie zijn external input → c31.c51_render_sxdoc.ts(SxDocument → HTML) + siblingc51_render_sxdoc.test.ts. Reden:content/sama/architecture.mdpicking-order regel 4 — "Does it produce HTML? Yes → c51". sxToHtml produceert HTML.- Correctie t.o.v. eerder plan + research-migration: parser/renderer waren foutief op c32 geplaatst (research keek alleen naar verifier-hard-rule "c32 vereist sibling-test", maar canon-docs sturen anders). Tests blijven (c31 sibling is informationally verplicht via Modeled; c51 idem voor goed onderhoud al staat het niet hard in de verifier).
Commit-vorm: één multi-path commit per save
c14_gitkrijgt nieuwecommitFiles(paths: Array<{path, body}>)naast bestaandecommitFile.- Eén commit → atomic rollback van die SHA herstelt beide bestanden.
Werkwijze (build-discipline per file-landing)
Elke file-write moet alle vier SAMA-axes passeren vóór de volgende file landt. Geen pile-up van violations.
| Axis | Wat dat afdwingt | Hoe we dat afdwingen |
|---|---|---|
| Sorted | c1*/c3* mogen niet relatief upward importeren | Bottom-up bouwen: c1 → c3 → c2 → c5. Nooit import naar hogere laag. |
| Architecture | prefix ∈ {11, 13, 14, 21, 31, 32, 51} | Layer-toewijzing vóór tik. I/O? → c14. Logic+transform? → c32. Pure types/registry? → c31. |
| Modeled | c32_*.ts vereist sibling .test.ts (hard); c31 = info-only | c32 source + test landen in dezelfde edit-batch, nooit los. Test heeft ≥1 expect() per test(...)-body. |
| Atomic | ≤700 LOC per file; geen placeholder tests | wc -l checken vóór commit. Splits gebudgetteerd (client/render per block-kind; shortcodes registry+substitute). |
Niet-verifier-afgedwongen SAMA-canon (per content/sama/*.md)
- Flat
src/— geen subdirs server-side. Client ondersrc/client/**.ts(buiten verifier-glob). - Geen barrel re-exports (
atomic.md). - c31/c32 importeren geen I/O-modules (sharp, fs, bun:sqlite, fetch) — verifier ziet alleen relative imports, dus dit is persoonlijke discipline.
- One concept per file — types apart van parser apart van renderer.
Verificatie-cadans
Na elke file-landing (niet alleen aan eind van fase):
bun test src/c32_sama_verify.test.ts # verifier zelf groen?
bun test src/<file>.test.ts # nieuwe sibling-test groen?
wc -l src/c*<file>*.ts # geen file > 700
Aan einde van elke fase:
bun test # alles groen
bun run src/c11_server.ts & # boot smoke
curl localhost:3000/health # 200
Anti-patronen (expliciet verboden)
- "Het werkt, test komt later" voor c32 — source en test landen samen of niet.
- Refactoren van lower-layer code in een higher-layer fase — bv.
c14_git.commitFilestoevoegen tijdens Fase 1 omdat het "handig" is. Lower-layer changes horen bij de fase waar de caller landt. - Sub-folders onder
src/server-side om "het netter te organiseren". Flatten is SAMA-canon. - Improviseren over layer-toewijzing — als je twijfelt over c31 vs c32, default naar c32 (sibling-test = vangnet).
Layer-correcties uit research-migration.md
Plan.md zat op drie plaatsen fout volgens de SAMA-rules in c32_sama_verify.ts. Gecorrigeerd:
| Was | Wordt | Reden |
|---|---|---|
c31_image_resize.ts |
c14_image_resize.ts |
sharp doet I/O — c14 verplicht |
c31_ai_edit_block.ts |
c14_openrouter.ts (HTTP) + c32_ai_edit_block.ts (validate/transform) |
OpenRouter HTTP = c14; orchestratie + sibling-test = c32 |
c31_sxdoc_parse.ts |
c32_sxdoc_parse.ts |
logica, geen pure types — c32 vereist sibling-test |
c31_sxdoc_render.ts |
c32_sxdoc_render.ts |
idem |
Atomic-700 splits gebudgetteerd
| File | LOC bij directe port | Splits |
|---|---|---|
sx-editor/src/client/render.ts |
775 | over Atomic-700; split per block-kind onder src/client/blocks/render-{p,h,list,quote,code,img,html,shortcode}.ts + één src/client/render.ts-dispatch ≤200 LOC |
sx-filter/src/shortcodes.ts |
650 | krap; pak meteen split langs c31_shortcodes_registry.ts (built-ins) + c32_shortcodes_substitute.ts (HTML-rewriter met regio-skip) |
Tests-zijn-siblings rule (was niet geëxpliciteerd)
Podman's sx-editor/tests/unit.test.ts shape incompatibel. Elke cXX_*.test.ts moet als sibling naast cXX_*.ts staan onder src/. Bestaande tdd.md tests doen dit al correct.
Client-side placement: src/client/**.ts
Geen verifier-impact (alleen cXX_*.ts wordt gescand). Relatieve imports naar ../c31_sxdoc.ts werken vanuit hier. Bun.build bundelt uit src/client/. Geen nieuwe top-level dir.
Verboden subdirs onder src/
Podman's sxdoc/, core/, db/, client/blocks/ mag niet onder src/ blijven bij server-port. Dat geldt niet voor src/client/ (die staat buiten verifier-scope). Server-code flat houden.
SAMA-mapping — podman-stuk → tdd.md cXX-laag
SAMA-conventie (per memory): cXX_*.ts, c1X = data/I-O, c2X = handlers/app, c3X = pure logic, c5X = render. Lower layer never imports higher.
| Podman | tdd.md (nieuw) | SAMA-laag | Wat het doet |
|---|---|---|---|
sx-data/sx.db schema |
c13_database.ts (extend) |
c1 | tabellen sx_documents, media, content_index, api_keys |
sx-editor/src/sxdoc/types.ts |
c31_sxdoc.ts |
c3 | SxDocument, Block, helpers — pure types/registry |
sx-editor/src/sxdoc/html-to-sx.ts |
c31_sxdoc_parse.ts (+ sibling .test.ts) |
c3 | HTML → SxDocument (parser = c31 per Modeled.md) |
sx-editor/src/sxdoc/sx-to-html.ts |
c51_render_sxdoc.ts (+ sibling .test.ts) |
c5 | SxDocument → HTML (produces HTML = c51 per Architecture.md) |
sx-editor/src/sxdoc/db.ts |
c13_database.ts extend (saveDocument/loadDocument/listDocuments/deleteDocument) |
c1 | SQLite read/write (canon-B); bun:sqlite = c13, niet c14 |
sx-editor/src/upload.ts + sharp resize |
c14_media.ts + c14_image_resize.ts |
c1 | upload, on-disk store, sharp transforms (sharp = I/O) |
sx-editor/src/ai.ts (OpenRouter) |
c14_openrouter.ts + c32_ai_edit_block.ts |
c1 + c3 | HTTP-call in c14; validate + transform in c32 met sibling-test |
sx-editor/src/templates.ts (list/edit shells) |
c51_render_admin.ts |
c5 | admin-list + edit-page chrome |
sx-editor/src/routes.ts (urlForPage/Post) |
bestaande c31_site_config.ts extend |
c3 | routes.yaml-equivalent — wij hebben al routes-config |
sx-editor/src/client/blockeditor.ts + slashmenu.ts + blocks/* |
client/ (TS bundle) → served door c21_handlers_edit.ts |
client | block-editor JS, slash-menu, AI ✨, autosave |
sx-editor/src/build.ts (Bun.build serve) |
c14_client_bundle.ts |
c1 | bundle TS-client → ESM, cache in geheugen |
sx-filter/src/shortcodes.ts (650 LOC — over 700 binnen 1 add) |
split: c31_shortcodes_registry.ts (built-ins, namen, args) + c32_shortcodes_substitute.ts (HTML-rewriter met meta/script-skip) + verplichte .test.ts op de c32 |
c3 | parsing/substitutie, voorkomt Atomic-700 violation |
sx-filter/src/admin.ts (admin-button injectie) |
bestaande edit-flow heeft al login-gate | c2 | n.v.t. — wij hebben echte auth |
sx-content/src/render.ts (Handlebars renderer) |
c51_render_theme.ts |
c5 | Ghost-compat theme renderer; geen Handlebars-dep — pure TS template-helpers |
sx-content/src/sitemap.ts |
c51_render_sitemap.ts |
c5 | sitemap.xml + RSS |
sx-content/src/images.ts |
onderdeel van c14_media.ts boven |
c1 | path-routed /content/images/* |
sx-themes/syntax/*.hbs partials |
theme/*.html of c51_render_theme_partials.ts |
c5 | Ghost-look, maar als TS template-helpers |
Nieuwe handlers (c21)
c21_handlers_admin_list.ts—/admin/lijst van pages+postsc21_handlers_admin_edit.ts—/admin/edit/{type}/{slug}(block-editor)c21_handlers_admin_new.ts—/admin/newc21_handlers_admin_upload.ts—/admin/uploadc21_handlers_admin_ai.ts—/admin/ai/edit-blockc21_handlers_admin_preview.ts—/admin/preview(live render)c21_handlers_content.ts— public render dispatcher (post/page/tag/author)c21_handlers_sitemap.ts—/sitemap.xml,/blog/rss/c21_handlers_media.ts—/content/images/*
Bestaande c21_handlers_edit.ts wordt vervangen door c21_handlers_admin_edit.ts (block-editor i.p.v. textarea).
Content-migratie
Bestaande tdd.md content:
content/home.md
content/blog/*.md (9 posts)
content/sama/*.md (5 pages)
content/games/*/ (2 games — multi-file)
content/guides/*.md (3 pages)
content/git-history/* (commit-meta JSON)
Migratie-strategie (canon B, SQLite + git-mirror):
- Eenmalig script
scripts/migrate_content_to_sxdoc.ts(loopt lokaal, niet in container). - Voor elke
.md: lees frontmatter (titel, tags, status), parseer body →SxDocumentviac32_sxdoc_parse, insert insx_documentstabel, schrijf óók*.sxdoc.jsonernaast voor git-mirror. home.md→ slug_home(matcht podman's special_homeslug).- Games (
content/games/*/) blijven multi-file — buiten CMS-scope, blijven viac31_games.tsgerenderd. git-history/is geen content — geen migratie nodig.- Eén batch-commit: "Migrate: content → sxdoc (SQLite-canon + git-mirror)" met alle
*.sxdoc.jsontoevoegingen.
Public URLs blijven gelijk (deze zijn al via c31_site_config gerouteerd). De Ghost-style /blog/{primary_tag}/{slug}/ permalink is optioneel en gaat door de redirects-laag die we al hebben.
Fasering
Per memory: bypass-pacing / JOLO is OK voor scopes die in één run passen. Dit is een dagen-werk port, dus ik fasering aanhouden met deploy + verify per fase.
Fase 0 — beslissing + scaffolding (afgerond 2026-05-11)
- Plan vastleggen (dit document).
- Storage-canon bevestigd: B (SQLite-canon + git-audit-mirror).
- Parser/render laag bevestigd: c32.
- Commit-vorm bevestigd: één multi-path commit per save.
- Research-migration onderzoek afgerond →
research-migration.md. - Layer-correcties verwerkt in mapping-tabel.
-
plan.mdcommitten (wacht op user-go).
Fase 1 — sxdoc-fundament (in uitvoering 2026-05-11)
-
c31_sxdoc.ts— types only (geen sibling-test verplicht) -
c31_sxdoc_parse.ts(HTML→tree, port van podmanhtml-to-sx.ts) + siblingc31_sxdoc_parse.test.ts -
c51_render_sxdoc.ts(tree→HTML, port van podmansx-to-html.ts) + siblingc51_render_sxdoc.test.ts - Skip typed marketing blocks — niet nodig voor tdd.md content (~600 LOC bespaard).
-
c13_database.tsextend:sx_documentstabel + saveDocument/loadDocument/listDocuments/deleteDocument -
package.json:node-html-parsertoegevoegd -
bun install— [email protected] binnen -
bun test src/c31_sxdoc_parse.test.ts src/c51_render_sxdoc.test.ts— 53/53 ✓ -
bun test src/c32_sama_verify.test.ts— 10/10 ✓ (verifier zelf groen) -
bun test(full suite) — 120/120 ✓ (67 pre-Fase-1 + 53 nieuwe) -
wc -lop nieuwe files — hoogste 327 LOC (c31_sxdoc_parse), c13_database 390 LOC; allemaal < 700 -
bun run src/c11_server.tsboot-smoke OK —/en/samabeide 200 c14_client_bundle.ts(Bun.build memoised) — komt pas in Fase 2- Geen route-impact — alles puur unit-getest, niets aan de live site veranderd.
Fase 1 gates passed 2026-05-11. Sxdoc-fundament SAMA-canon compliant en groen.
Fase 2 — admin-UI
2a — server-side CRUD (afgerond 2026-05-11):
-
c31_admin_validation.ts+ sibling test (14/14 groen) — parser/validator per Modeled.md -
c51_render_admin.ts— list + edit form + login/non-admin walls -
c21_handlers_admin.ts— adminListHandler, adminNewHandler, adminEditHandler, adminDeleteHandler (één bestand i.p.v. plan-spec 4; matcht bestaandec21_handlers_agents/c21_handlers_authpattern, 218 LOC) -
c21_app.tsroutes: /admin, /admin/new, /admin/edit/:type/:slug, /admin/delete/:type/:slug - Boot-smoke: anonymous → 401, login-wall rendert ✓
- ⚠
c21_app.tsis nu 702 LOC (Atomic-grens 700 overschreden door 2 regels). Vraagt aparte split-refactor — c21_handlers_projects.ts, c21_handlers_api_agents.ts, c21_handlers_webhook.ts uit het inline-deel halen.
2b — client-side block editor (afgerond 2026-05-11):
-
c14_client_bundle.ts— Bun.build memoised + ETag, 72 LOC -
src/client/blockeditor.ts— hydratie + state + autosave + raw-mode toggle, 336 LOC -
src/client/slashmenu.ts— filterable popup met arrows/enter/escape, 161 LOC -
src/client/blocks.ts— per-block-kind renderers (p, h, ul, ol, quote, code, img, hr, html, shortcode), inline marks parser, slash-trigger, 393 LOC (één file ipvblocks/*— onder Atomic-700) -
c51_render_admin.ts— neemt nu SxDocument als input, projecteert naar textarea-HTML + embedt<script id="sxdoc-initial">JSON, laadt bundle<script type="module" src="/admin/assets/blockeditor.js"> -
c21_handlers_admin.ts— JSON autosave path (Accept: application/json→{ok:true,ts,slug,type}) -
c21_app.tsroute/admin/assets/blockeditor.jsmet ETag/304 -
public/style.cssadmin-editor sectie toegevoegd (~190 LOC editor + slashmenu + toast) - Bundle compileert (26KB), serves 200, ETag → 304 ✓
- Boot-smoke: /admin nog 401 anoniem (auth-gate intact) ✓
- Full suite 134/134 ✓
- E2E spec
e2e/admin-block-editor.spec.ts— uitgesteld; needs admin-sessie helper, beter in apart turn - Deploy + verify op p620 — wacht op user-go
Tech-debt uit Fase 2:
- c21_app.ts is nu 716 LOC (Atomic-grens 700 overschreden door 16). Bestond al lang voor admin port; mijn route-adds duwden 't over. Splitsen langs
c21_handlers_projects.ts/c21_handlers_api_agents.ts/c21_handlers_webhook.tspatroon (al gebruikt voor agents/auth/reports/sama) — aparte refactor, niet Fase 3-blocker.
Fase 3 — media + AI
c14_media.ts(upload + on-disk store ondercontent/images/)c14_image_resize.ts(sharp wrapper — sharp is I/O = c14)c21_handlers_media.ts(GET /content/images/...)c21_handlers_admin_upload.ts(slash-menu image card target)c14_openrouter.ts(HTTP-call) +c32_ai_edit_block.ts(validate+transform, sibling-test verplicht) +c21_handlers_admin_ai.ts(✨ button)- E2E: image upload + AI edit
- Deploy + verify
Fase 4 — public renderer + Ghost-look theme
c51_render_theme.ts(port van podman's Handlebars partials naar TS template-helpers, geen Handlebars-dep)c51_render_theme_partials.ts(nav, footer, post-card, post-list)c31_shortcodes_registry.ts(built-in lijst + arg-schemas)c32_shortcodes_substitute.ts(HTML-rewriter met meta/script-skip) +.test.tsc21_handlers_content.tsswap: huidige render-paden → nieuwe theme- CSS port:
sx-themes/syntax/assets/*→public/style.css(uitgebreid) - E2E: visuele parity-checks (
e2e/theme-parity.spec.ts) - Deploy + verify
Fase 5 — sitemap, RSS, live-preview
c51_render_sitemap.ts+c21_handlers_sitemap.tsc21_handlers_admin_preview.ts+ live-preview iframe in admin- Deploy + verify
Fase 6 — content-migratie + cutover
scripts/migrate_content_to_sxdoc.tslokaal draaien- Commit alle
*.sxdoc.jsonals één migratie-batch - Verwijder oude
c21_handlers_edit.ts+c51_render_edit.ts(block-editor is canoniek) - SAMA verify groen (
c32_sama_verifyover alle nieuwe files) - Deploy + visual diff t.o.v. pre-migratie
- Memory updaten: "tdd.md CMS = sxdoc block-editor, Ghost-compat theme, git-canon (of SQLite-canon)"
Risico's
| Risico | Mitigatie |
|---|---|
| Block-editor JS is groot (slashmenu + 7 block types) | Bundle on-demand via c14_client_bundle, cache in memory. Geen build-step buiten Bun.build. |
Ghost-Handlebars helpers ({{#foreach}}, {{date}}, {{img_url}}) — handgeschreven her-implementeren |
Klein arsenaal nodig; allemaal in c51_render_theme.ts + unit-tested. Geen Handlebars-dep. |
| SAMA verify struikelt over client/ bestanden | client/ valt buiten cXX-naming-regel; al supported (zie bestaande e2e/). |
| Content-migratie loss-y voor markdown met embedded HTML | sxdoc heeft escape-hatch html block; parser valt daarop terug. Visuele parity-check in Fase 6. |
| Sharp binary in container | Bestaat al niet in tdd.md. Quadlet image-build moet sharp meebakken — eenmalige Dockerfile-edit. |
OPENROUTER_API_KEY ontbreekt in prod |
AI ✨ wordt 503 met hint (zoals podman). Niet blokkerend. |
Tellen
- Podman LOC (sx-editor + sx-content + sx-filter,
src/only): ~6-8k geschat. - Tdd.md LOC nu: 7.5k.
- Port voegt geschat 4-6k toe (geen Handlebars-overhead, hergebruik bestaande c13/c14/c32-laag).
- Eindstand: ~12-13k LOC, één Bun-proces, één SQLite-file, één bare repo. Geen extra services.
Open vragen (kunnen later)
Voor Fase 1 zijn de gelockte beslissingen voldoende. Deze vragen worden relevant per fase:
Storage✅ B (SQLite-canon + git-mirror) — locked.- Permalink-vorm (Fase 4):
/blog/{primary_tag}/{slug}/(Ghost-style) of/blog/{slug}/(huidig)? Aanbevolen: huidig behouden, 9 bestaande URLs blijven werken. - AI element-edit in prod (Fase 3):
OPENROUTER_API_KEYop p620 zetten, of alleen lokaal/dev (503 in prod)? - Games (Fase 6): buiten CMS via bestaande
c31_games.ts, of óók via sxdoc? Aanbevolen: buiten. - Ghost Content API endpoints (
/ghost/api/content/...): meeporten? Aanbevolen: drop, bespaart ~150 LOC. Tdd.md heeft geen externe API-consumers. - Marketing-blocks (hero/feature-card/etc.): meeporten of skip? Aanbevolen: skip — niet nodig voor tdd.md content, scheelt ~600 LOC.