syntaxai/tdd.md · main · plan.md

plan.md 322 lines · 20666 bytes 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, 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/<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.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)

  • 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.md committen (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 podman html-to-sx.ts) + sibling c31_sxdoc_parse.test.ts
  • c51_render_sxdoc.ts (tree→HTML, port van podman sx-to-html.ts) + sibling c51_render_sxdoc.test.ts
  • Skip typed marketing blocks — niet nodig voor tdd.md content (~600 LOC bespaard).
  • c13_database.ts extend: sx_documents tabel + saveDocument/loadDocument/listDocuments/deleteDocument
  • package.json: node-html-parser toegevoegd
  • 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 -l op nieuwe files — hoogste 327 LOC (c31_sxdoc_parse), c13_database 390 LOC; allemaal < 700
  • 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):

  • 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 bestaande c21_handlers_agents/c21_handlers_auth pattern, 218 LOC)
  • c21_app.ts routes: /admin, /admin/new, /admin/edit/:type/:slug, /admin/delete/:type/:slug
  • 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):

  • 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 ipv blocks/* — 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.ts route /admin/assets/blockeditor.js met ETag/304
  • public/style.css admin-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.ts patroon (al gebruikt voor agents/auth/reports/sama) — aparte refactor, niet Fase 3-blocker.

Fase 3 — media + AI

  • c14_media.ts (upload + on-disk store onder content/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.ts
  • c21_handlers_content.ts swap: 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.ts
  • c21_handlers_admin_preview.ts + live-preview iframe in admin
  • Deploy + verify

Fase 6 — content-migratie + cutover

  • scripts/migrate_content_to_sxdoc.ts lokaal draaien
  • Commit alle *.sxdoc.json als één migratie-batch
  • Verwijder oude c21_handlers_edit.ts + c51_render_edit.ts (block-editor is canoniek)
  • SAMA verify groen (c32_sama_verify over 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:

  1. Storage ✅ B (SQLite-canon + git-mirror) — locked.
  2. Permalink-vorm (Fase 4): /blog/{primary_tag}/{slug}/ (Ghost-style) of /blog/{slug}/ (huidig)? Aanbevolen: huidig behouden, 9 bestaande URLs blijven werken.
  3. AI element-edit in prod (Fase 3): OPENROUTER_API_KEY op p620 zetten, of alleen lokaal/dev (503 in prod)?
  4. Games (Fase 6): buiten CMS via bestaande c31_games.ts, of óók via sxdoc? Aanbevolen: buiten.
  5. Ghost Content API endpoints (/ghost/api/content/...): meeporten? Aanbevolen: drop, bespaart ~150 LOC. Tdd.md heeft geen externe API-consumers.
  6. Marketing-blocks (hero/feature-card/etc.): meeporten of skip? Aanbevolen: skip — niet nodig voor tdd.md content, scheelt ~600 LOC.