website-htmx-rustelo/.ontoref/adrs/adr-001-single-image-deploy.ncl
Jesús Pérez 3ccd15a14b init repo
2026-07-10 03:33:46 +01:00

97 lines
6.6 KiB
XML

# ADR-001 — Single-Image Deploy for HTMX-SSR Consumers
# Plain record (the framework has no ADR contract tooling yet); mirrors the
# constellation _template.ncl shape so it can graduate to the typed schema later.
{
id = "adr-001",
title = "Single-Image Deploy for HTMX-SSR Consumers — Baked Route Superset, Runtime Per-Site Substance",
status = 'Accepted,
date = "2026-06",
context = m%"
`website-htmx-rustelo/code` builds the shared `rustelo-htmx-server` binary that
multiple sites consume (the personal bilingual site and ontoref/outreach/site).
Historically the binary WAS one specific site: site/config (routes, menus, theme,
logo, languages) is consumed at COMPILE TIME via build.rs/build-config codegen, so
a new consumer had to edit the framework repo and rebuild to get their site. This
is the framework-vs-one-site tension realised as friction: "we are incapable of
creating an outreach/site using this framework as a base without modifying and
recompiling it." Investigation (2026-06-14) showed the question is not which pole
to pick but where to draw the seam: most per-site SUBSTANCE is already runtime
(FTL deferred in htmx-ssr; SITE_BASE_URL, SITE_NAME, SUPPORTED_LANGUAGES via env;
menus/footer loaded from routes.ncl/footer.ncl at startup; brand CSS via custom.css;
templates via HTMX_TEMPLATE_PATH), while the route TABLE is compiled into the binary
by the rustelo routing engine for type-safety and performance.
"%,
decision = m%"
Deploy as a SINGLE shared image, not a per-site build. Bake the SHARED STRUCTURE
(the route table, as a deliberate framework-level superset of all standard content
kinds and pages) and supply every per-site SUBSTANCE knob at runtime: brand/logo
(SITE_LOGO_LIGHT/DARK/ALT), name (SITE_NAME), public origin (SITE_BASE_URL),
languages (SUPPORTED_LANGUAGES), content, templates, CSS, menus/footer (consumer
routes.ncl/footer.ncl). A consumer runs the prebuilt binary, points env + supplies
config/content, and gets their site without a rebuild. Routing stays compiled — it
is NOT moved to runtime.
"%,
rationale = [
{ claim = "Per-site build is adoption-hostile",
detail = "Compiling per site forces every adopter to have the full Rust toolchain plus the stratumiops/foundation path deps and rebuild on each config change — contradicting the voluntary-adoption axiom and ADR-038 (single OCI image). A prebuilt image + consumer config is near-zero adoption cost." },
{ claim = "Runtime routing would fight the framework's grain",
detail = "The route table is generated into compiled code by the rustelo engine for type-safety and performance. Making it fully dynamic is a rearchitecture of rustelo core — the forbidden collapse to the all-dynamic pole." },
{ claim = "Sites differ in content/brand, not route structure",
detail = "Confirmed with the maintainer: future sites share the route shape (home/about/blog/projects/adr/catalog/...) and differ in content + brand. So one baked superset serves them all; unused routes simply carry no content and hide from the runtime menu." },
{ claim = "The runtime-substance pattern already exists",
detail = "FTL is already deferred to runtime in htmx-ssr; menus/footer already load from routes.ncl at startup; origin/name/languages are already env. Only the logo remained baked — now overridable via SITE_LOGO_* — so the model is mechanism-complete for substance." },
],
consequences = {
positive = [
"One image serves N sites; adopters need only config + content, no toolchain.",
"Brand/content changes never require a framework rebuild (only a shared-code bug or a new framework capability does).",
"Aligns with ADR-038 (OCI single image) and protocol-not-runtime.",
],
negative = [
"The baked route table must be maintained as a deliberate SUPERSET; a consumer menu/link to a route absent from the superset 404s (guarded by validate-route-superset).",
"Per-site correctness depends on config discipline (the consumer must supply its own brand env + content); the seam must be documented (.claude/CLAUDE.md) and verified (validate-seam).",
],
},
alternatives_considered = [
{ option = "Per-site build / image", why_rejected = "Adoption-hostile (toolchain + path deps + rebuild per config change); contradicts voluntary-adoption and ADR-038." },
{ option = "Fully runtime-dynamic routing (one binary, routes loaded at startup)", why_rejected = "Rearchitects the rustelo routing engine; loses compiled-route type-safety/perf; the forbidden all-dynamic collapse." },
],
constraints = [
{ id = "routing-stays-baked",
claim = "The route table is compiled; it is never moved to runtime loading.",
scope = "crates/server (routing), site/config/routes.ncl",
severity = 'Hard,
check = { tag = 'Grep, pattern = "ROUTES_REGISTRY|resource_contributor", path = "crates/server/src/resources.rs" },
rationale = "Preserves rustelo's compiled-route type-safety and performance; the superset is the unit of structure-sharing." },
{ id = "per-site-substance-is-runtime",
claim = "Brand, logo, name, origin, languages, content, templates and menus are supplied at runtime by the consumer, never baked per site.",
scope = "crates/server/src/theme.rs, shell, consumer run.sh + site/config",
severity = 'Hard,
check = { tag = 'NuCmd, cmd = "just validate-seam", expect_exit = 0 },
rationale = "A shared binary must carry each site's substance via runtime config; validate-seam rejects substance leaking into shared Rust." },
{ id = "route-table-is-a-superset",
claim = "site/config/routes.ncl declares ContentIndex + PostViewer for every standard content kind (blog, projects, adr, catalog) so any consumer's links resolve.",
scope = "site/config/routes.ncl",
severity = 'Hard,
check = { tag = 'NuCmd, cmd = "just validate-route-superset", expect_exit = 0 },
rationale = "A consumer menu/link to a kind absent from the baked superset 404s — the build/runtime split gotcha." },
],
related_adrs = [
"ontoref adr-038 (OCI distribution — single runnable image)",
"ontoref adr-029 (tier coexistence — optional accelerator, protocol-not-runtime)",
],
ontology_check = {
decision_string = "Single shared image: bake the route superset (structure), runtime the per-site substance — neither collapse to one site baked nor to fully-dynamic routing.",
invariants_at_risk = ["framework-vs-one-site", "formalization-vs-adoption", "consume-not-fork"],
verdict = 'Safe,
},
}