# 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, replay `git log`, terug. - Voordeel: "SAMA meets git" verhaal blijft kloppen. `sama-meets-git-cms.md` blijft 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/*.md` wordt eenmalig geïmporteerd naar `sx_documents` + `posts` tabellen, 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_documents` tabel in `c13_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 blijft `git log content/` de menselijk-leesbare audit-trail; "elke save = een commit" uit `sama-meets-git-cms.md` blijft 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.ts` leest huidige `content/**/*.md` → parseert naar `SxDocument` → schrijft SQLite + emit één migratie-commit met alle nieuwe `.sxdoc.json` ernaast. ### Parser laag: **c31** · Render laag: **c51** - `c31_sxdoc_parse.ts` (HTML → SxDocument) + sibling `c31_sxdoc_parse.test.ts`. Reden: `content/sama/modeled.md` is 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) + sibling `c51_render_sxdoc.test.ts`. Reden: `content/sama/architecture.md` picking-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_git` krijgt nieuwe `commitFiles(paths: Array<{path, body}>)` naast bestaande `commitFile`. - 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 onder `src/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/.test.ts # nieuwe sibling-test groen? wc -l src/c**.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.commitFiles` toevoegen 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+posts - `c21_handlers_admin_edit.ts` — `/admin/edit/{type}/{slug}` (block-editor) - `c21_handlers_admin_new.ts` — `/admin/new` - `c21_handlers_admin_upload.ts` — `/admin/upload` - `c21_handlers_admin_ai.ts` — `/admin/ai/edit-block` - `c21_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): 1. **Eenmalig script** `scripts/migrate_content_to_sxdoc.ts` (loopt lokaal, niet in container). 2. Voor elke `.md`: lees frontmatter (titel, tags, status), parseer body → `SxDocument` via `c32_sxdoc_parse`, **insert** in `sx_documents` tabel, schrijf óók `*.sxdoc.json` ernaast voor git-mirror. 3. `home.md` → slug `_home` (matcht podman's special `_home` slug). 4. Games (`content/games/*/`) blijven multi-file — buiten CMS-scope, blijven via `c31_games.ts` gerenderd. 5. `git-history/` is geen content — geen migratie nodig. 6. Eén batch-commit: "Migrate: content → sxdoc (SQLite-canon + git-mirror)" met alle `*.sxdoc.json` toevoegingen. 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) - [x] Plan vastleggen (dit document). - [x] Storage-canon bevestigd: **B (SQLite-canon + git-audit-mirror)**. - [x] Parser/render laag bevestigd: **c32**. - [x] Commit-vorm bevestigd: **één multi-path commit per save**. - [x] Research-migration onderzoek afgerond → `research-migration.md`. - [x] Layer-correcties verwerkt in mapping-tabel. - [ ] `plan.md` committen (wacht op user-go). ### Fase 1 — sxdoc-fundament (in uitvoering 2026-05-11) - [x] `c31_sxdoc.ts` — types only (geen sibling-test verplicht) - [x] `c31_sxdoc_parse.ts` (HTML→tree, port van podman `html-to-sx.ts`) + sibling `c31_sxdoc_parse.test.ts` - [x] `c51_render_sxdoc.ts` (tree→HTML, port van podman `sx-to-html.ts`) + sibling `c51_render_sxdoc.test.ts` - [x] Skip typed marketing blocks — niet nodig voor tdd.md content (~600 LOC bespaard). - [x] `c13_database.ts` extend: `sx_documents` tabel + saveDocument/loadDocument/listDocuments/deleteDocument - [x] `package.json`: `node-html-parser` toegevoegd - [x] `bun install` — node-html-parser@7.1.0 binnen - [x] `bun test src/c31_sxdoc_parse.test.ts src/c51_render_sxdoc.test.ts` — 53/53 ✓ - [x] `bun test src/c32_sama_verify.test.ts` — 10/10 ✓ (verifier zelf groen) - [x] `bun test` (full suite) — 120/120 ✓ (67 pre-Fase-1 + 53 nieuwe) - [x] `wc -l` op nieuwe files — hoogste 327 LOC (c31_sxdoc_parse), c13_database 390 LOC; allemaal < 700 - [x] `bun run src/c11_server.ts` boot-smoke OK — `/` en `/sama` beide 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):** - [x] `c31_admin_validation.ts` + sibling test (14/14 groen) — parser/validator per Modeled.md - [x] `c51_render_admin.ts` — list + edit form + login/non-admin walls - [x] `c21_handlers_admin.ts` — adminListHandler, adminNewHandler, adminEditHandler, adminDeleteHandler (één bestand i.p.v. plan-spec 4; matcht bestaande `c21_handlers_agents`/`c21_handlers_auth` pattern, 218 LOC) - [x] `c21_app.ts` routes: /admin, /admin/new, /admin/edit/:type/:slug, /admin/delete/:type/:slug - [x] Boot-smoke: anonymous → 401, login-wall rendert ✓ - ⚠ `c21_app.ts` is 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):** - [x] `c14_client_bundle.ts` — Bun.build memoised + ETag, 72 LOC - [x] `src/client/blockeditor.ts` — hydratie + state + autosave + raw-mode toggle, 336 LOC - [x] `src/client/slashmenu.ts` — filterable popup met arrows/enter/escape, 161 LOC - [x] `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 ipv `blocks/*` — onder Atomic-700) - [x] `c51_render_admin.ts` — neemt nu SxDocument als input, projecteert naar textarea-HTML + embedt `