From 3ccd15a14b4db1bf4b158018d72701cf53a854b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesu=CC=81s=20Pe=CC=81rez?= Date: Fri, 10 Jul 2026 03:33:46 +0100 Subject: [PATCH] init repo --- .gitignore | 17 ++ .ontoref/adrs/adr-001-single-image-deploy.ncl | 97 ++++++++++++ .ontoref/card.ncl | 25 +++ .ontoref/config.ncl | 39 +++++ .ontoref/ontology/connections.ncl | 30 ++++ .ontoref/ontology/core.ncl | 139 +++++++++++++++++ .ontoref/ontology/gate.ncl | 83 ++++++++++ .ontoref/ontology/manifest.ncl | 74 +++++++++ .ontoref/ontology/purpose-constraints.ncl | 120 ++++++++++++++ .ontoref/ontology/purpose-defaults.ncl | 28 ++++ .ontoref/ontology/purpose-schema.ncl | 98 ++++++++++++ .ontoref/ontology/purpose.ncl | 146 ++++++++++++++++++ .ontoref/ontology/schemas/project-card.ncl | 37 +++++ .ontoref/ontology/state.ncl | 139 +++++++++++++++++ .ontoref/project.ncl | 14 ++ .ontoref/reflection/constraints.ncl | 51 ++++++ .ontoref/reflection/defaults.ncl | 28 ++++ .../reflection/modes/plan-publication.ncl | 70 +++++++++ .ontoref/reflection/modes/publish-content.ncl | 98 ++++++++++++ .ontoref/reflection/modes/sync-ontology.ncl | 44 ++++++ .../modes/validate-config-drift.ncl | 37 +++++ .../reflection/modes/validate-content.ncl | 47 ++++++ .../reflection/modes/validate-hydration.ncl | 46 ++++++ .../modes/validate-neutral-binary.ncl | 35 +++++ .../modes/validate-purpose-alignment.ncl | 111 +++++++++++++ .../modes/validate-route-superset.ncl | 37 +++++ .ontoref/reflection/modes/validate-seam.ncl | 68 ++++++++ .ontoref/reflection/schema.ncl | 134 ++++++++++++++++ .ontoref/reflection/schemas/backlog.ncl | 33 ++++ .rustelo.ontoref/README.md | 29 ++++ .rustelo.ontoref/app.ncl | 47 ++++++ README.md | 0 32 files changed, 2001 insertions(+) create mode 100644 .gitignore create mode 100644 .ontoref/adrs/adr-001-single-image-deploy.ncl create mode 100644 .ontoref/card.ncl create mode 100644 .ontoref/config.ncl create mode 100644 .ontoref/ontology/connections.ncl create mode 100644 .ontoref/ontology/core.ncl create mode 100644 .ontoref/ontology/gate.ncl create mode 100644 .ontoref/ontology/manifest.ncl create mode 100644 .ontoref/ontology/purpose-constraints.ncl create mode 100644 .ontoref/ontology/purpose-defaults.ncl create mode 100644 .ontoref/ontology/purpose-schema.ncl create mode 100644 .ontoref/ontology/purpose.ncl create mode 100644 .ontoref/ontology/schemas/project-card.ncl create mode 100644 .ontoref/ontology/state.ncl create mode 100644 .ontoref/project.ncl create mode 100644 .ontoref/reflection/constraints.ncl create mode 100644 .ontoref/reflection/defaults.ncl create mode 100644 .ontoref/reflection/modes/plan-publication.ncl create mode 100644 .ontoref/reflection/modes/publish-content.ncl create mode 100644 .ontoref/reflection/modes/sync-ontology.ncl create mode 100644 .ontoref/reflection/modes/validate-config-drift.ncl create mode 100644 .ontoref/reflection/modes/validate-content.ncl create mode 100644 .ontoref/reflection/modes/validate-hydration.ncl create mode 100644 .ontoref/reflection/modes/validate-neutral-binary.ncl create mode 100644 .ontoref/reflection/modes/validate-purpose-alignment.ncl create mode 100644 .ontoref/reflection/modes/validate-route-superset.ncl create mode 100644 .ontoref/reflection/modes/validate-seam.ncl create mode 100644 .ontoref/reflection/schema.ncl create mode 100644 .ontoref/reflection/schemas/backlog.ncl create mode 100644 .rustelo.ontoref/README.md create mode 100644 .rustelo.ontoref/app.ncl create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d25e85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ + +.coder/ +.claude/ +.internal.git/ +logs +logs-archive +.wrks +.DS_Store + +resources +examples +code +desktop +outreach +vault + +node_modules diff --git a/.ontoref/adrs/adr-001-single-image-deploy.ncl b/.ontoref/adrs/adr-001-single-image-deploy.ncl new file mode 100644 index 0000000..87e3111 --- /dev/null +++ b/.ontoref/adrs/adr-001-single-image-deploy.ncl @@ -0,0 +1,97 @@ +# 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, + }, +} diff --git a/.ontoref/card.ncl b/.ontoref/card.ncl new file mode 100644 index 0000000..9cd9dcb --- /dev/null +++ b/.ontoref/card.ncl @@ -0,0 +1,25 @@ +let d = import "ontology/schemas/project-card.ncl" in + +d.ProjectCard & { + id = "website-htmx-rustelo", + name = "Website HTMX (Rustelo)", + tagline = "Bilingual EN/ES website on Rustelo — HTMX-first rendering", + description = "Configuration-driven bilingual website implementing the Rustelo framework. Renders through the HTMX server-side profile with Minijinja templates; routes, themes, menus and content types are declared in NCL/TOML with zero hardcoding. Dual-mode capable (htmx-ssr / leptos-hydration).", + version = "0.1.0", + status = 'Beta, + source = 'Local, + url = "", + repo = "", + started_at = "2026", + tags = ["rust", "rustelo", "htmx", "nickel", "website", "i18n"], + tools = ["Rust", "Leptos", "Axum", "HTMX", "Minijinja", "Nickel", "UnoCSS"], + features = [ + "Config-driven routes, themes, menus (NCL/TOML)", + "Bilingual content (en, es) with Fluent i18n", + "HTMX server-side rendering profile (ADR-006) with Minijinja templates", + "Dual-mode build capability (htmx-ssr / leptos-hydration)", + ], + featured = false, + sort_order = 0, + logo = "", +} diff --git a/.ontoref/config.ncl b/.ontoref/config.ncl new file mode 100644 index 0000000..c0d5fd2 --- /dev/null +++ b/.ontoref/config.ncl @@ -0,0 +1,39 @@ +# .ontoref/config.ncl — ontoref configuration for website +{ + nickel_import_paths = [ + ".", + ".ontoref", + ".ontoref/ontology", + ".ontoref/ontology/schemas", + ".ontoref/adrs", + ".ontoref/reflection/schemas", + ".ontoref/reflection/requirements", + ], + + log = { + level = "info", + path = ".ontoref/logs", + rotation = "daily", + compress = false, + archive = ".ontoref/logs-archive", + max_files = 7, + }, + + mode_run = { + rules = [ + { when = { mode_id = "validate-ontology" }, allow = true, reason = "validation always allowed" }, + { when = { actor = "agent" }, allow = true, reason = "agent actor always allowed" }, + { when = { actor = "ci" }, allow = true, reason = "ci actor always allowed" }, + ], + }, + + nats_events = { + enabled = false, + url = "nats://localhost:4222", + emit = [], + subscribe = [], + handlers_dir = "reflection/handlers", + }, + + card = import "card.ncl", +} diff --git a/.ontoref/ontology/connections.ncl b/.ontoref/ontology/connections.ncl new file mode 100644 index 0000000..75725e4 --- /dev/null +++ b/.ontoref/ontology/connections.ncl @@ -0,0 +1,30 @@ +let s = import "reflection/schemas/connections.ncl" in + +{ + upstream = [ + { + project = "rustelo", + kind = 'LibraryDependency, + node = "config-driven-architecture", + via = "cargo", + note = "Implements the Rustelo framework: consumes framework crates as dependencies, uses the layered override system, custom routing, and the HTMX rendering profile. Declared as a RusteloApp via .rustelo.ontoref/.", + }, + { + project = "ontoref", + kind = 'LibraryDependency, + node = "protocol-as-standalone", + via = "local", + note = "Ontoref protocol adoption: .ontoref/ontology schemas, ADR lifecycle, reflection modes, and the rustelo domain extension.", + }, + ], + downstream = [], + peers = [ + { + project = "jpl-website", + kind = 'LibraryDependency, + node = "config-driven-architecture", + via = "local", + note = "Sibling Rustelo implementation (same bilingual-website archetype). Shares the rustelo domain reflection modes and PAP constraints.", + }, + ], +} | s.Connections diff --git a/.ontoref/ontology/core.ncl b/.ontoref/ontology/core.ncl new file mode 100644 index 0000000..036b13c --- /dev/null +++ b/.ontoref/ontology/core.ncl @@ -0,0 +1,139 @@ +let d = import "defaults/core.ncl" in + +{ + nodes = [ + + # ── Axioms (invariant = true) ───────────────────────────────────────────── + + d.make_node { + id = "consume-not-fork", + name = "Consume, Don't Fork", + pole = 'Yang, + level = 'Axiom, + description = "This project is an implementation of Rustelo, never a fork of it. All customization flows through the framework's layered override system (Local > Feature > Template > Framework) and the plugin architecture — framework crates are consumed as dependencies, never modified in place.", + invariant = true, + artifact_paths = ["Cargo.toml", "crates/"], + }, + + d.make_node { + id = "config-driven-content", + name = "Config-Driven Content & Routing", + pole = 'Yang, + level = 'Axiom, + description = "Routes, menus, themes, content types and rendering profiles are declared in NCL/TOML under site/config — never hardcoded in Rust. Languages are discovered from configuration, not literals.", + invariant = true, + artifact_paths = ["site/config/", "site/config/routes/"], + }, + + d.make_node { + id = "bilingual-parity", + name = "Bilingual Parity (EN/ES)", + pole = 'Yang, + level = 'Axiom, + description = "Every public route and content object exists in both English and Spanish, with Fluent FTL keys symmetric across locales. The site never ships a route present in one language but missing in the other.", + invariant = true, + artifact_paths = ["site/content/", "site/i18n/locales/"], + }, + + # ── Tensions ────────────────────────────────────────────────────────────── + + d.make_node { + id = "htmx-vs-hydration", + name = "HTMX SSR vs Leptos Hydration", + pole = 'Spiral, + level = 'Tension, + description = "The site selects the HTMX server-side rendering profile (ADR-006) for simplicity and crawlability, but Rustelo also supports Leptos WASM hydration. Synthesis (not collapse): rendering profile is a per-route configuration choice over compile-time-typed contracts, so the site can opt individual routes into hydration without abandoning the HTMX default.", + }, + + d.make_node { + id = "content-velocity-vs-parity", + name = "Content Velocity vs Bilingual Parity", + pole = 'Spiral, + level = 'Tension, + description = "Publishing EN content quickly conflicts with the parity axiom. Resolved by treating ES completion as a tracked publication blocker (validate-content mode) rather than relaxing the invariant — EN may be drafted ahead, but a route is not production-published until ES reaches parity.", + }, + + d.make_node { + id = "framework-vs-one-site", + name = "Framework Neutrality vs One-Site Convenience", + pole = 'Spiral, + level = 'Tension, + description = "code/ builds the shared rustelo-htmx-server binary consumed by multiple sites (the personal bilingual site and ontoref/outreach/site), yet site/config — routes, menus, theme, logo, languages — is consumed at compile time, so the binary IS one specific site. Synthesis (not collapse): the customization seam keeps brand, content, templates and env on the runtime side (per-consumer, no rebuild) while routes and identity stay build-baked; the direction of motion is from binary-is-one-site toward binary-serves-N-consumers-from-consumer-supplied-config, without pretending routing can be fully dynamic. Forbidden collapse: letting brand/content customization leak into crates/server/src (one-site capture) OR demanding every config be runtime-dynamic (losing the build-baked routing that is a real performance choice).", + }, + + # ── Practices ───────────────────────────────────────────────────────────── + + d.make_node { + id = "htmx-rendering-profile", + name = "HTMX Rendering Profile", + pole = 'Yang, + level = 'Practice, + description = "Routes render server-side via Rustelo's HTMX profile and Minijinja templates, with per-template-block rendering for htmx fragment swaps (HX-Target → CSS id → block name). Vendored htmx assets verified by lockfile at startup.", + artifact_paths = ["site/config/routes/", "crates/pages_htmx/"], + }, + + d.make_node { + id = "bilingual-content-system", + name = "Bilingual Content System", + pole = 'Yang, + level = 'Practice, + description = "Per-language content directories and Fluent FTL bundles, with route configs declared per locale. Content integrity is checked by the validate-content reflection mode (route symmetry, content file completeness, FTL key coverage).", + artifact_paths = ["site/content/en/", "site/content/es/", "site/i18n/locales/"], + }, + + d.make_node { + id = "config-driven-routes", + name = "Config-Driven Route Surface", + pole = 'Yang, + level = 'Practice, + description = "Route table, menus and themes defined in NCL/TOML and resolved by Rustelo's custom routing engine at build time. No Leptos Router imports, no hardcoded paths.", + artifact_paths = ["site/config/routes/", "site/config/menus/"], + }, + + d.make_node { + id = "layered-customization", + name = "Layered Customization", + pole = 'Yin, + level = 'Practice, + description = "Site-specific look and behavior layered on top of framework defaults via local overrides and feature templates (UnoCSS theme, partials, page components), keeping the framework upgrade path intact.", + artifact_paths = ["site/config/", "uno.config.ts", "templates/"], + }, + + d.make_node { + id = "customization-seam", + name = "Customization Seam — Right Layer per Change", + pole = 'Yin, + level = 'Practice, + description = "Consumer customization spans four mechanisms: content + templates + brand-CSS + env are runtime/config (no rebuild); routes, menus, theme, logo and languages are build-baked config; shell, SEO and dispatch are shared Rust touched only for bugs or new framework capabilities. Brand and content changes must not touch crates/server/src. The seam is documented in .claude/CLAUDE.md and is the transition condition for the consumer-neutrality FSM dimension.", + artifact_paths = ["site/config/", "crates/pages_htmx/templates/", ".claude/CLAUDE.md"], + }, + + ], + + edges = [ + + # Axioms → Practices via ManifestsIn + { from = "config-driven-content", to = "config-driven-routes", kind = 'ManifestsIn, weight = 'High }, + { from = "config-driven-content", to = "htmx-rendering-profile", kind = 'ManifestsIn, weight = 'Medium }, + { from = "bilingual-parity", to = "bilingual-content-system", kind = 'ManifestsIn, weight = 'High }, + { from = "consume-not-fork", to = "layered-customization", kind = 'ManifestsIn, weight = 'High }, + + # Tensions → resolving Practices + { from = "htmx-vs-hydration", to = "htmx-rendering-profile", kind = 'Resolves, weight = 'High, + note = "Synthesis: the site defaults to HTMX SSR but the profile is a per-route config choice, so hydration stays available without forking the framework." }, + { from = "content-velocity-vs-parity", to = "bilingual-content-system", kind = 'Resolves, weight = 'High, + note = "ES completion is tracked as a publication blocker, preserving the parity invariant while allowing EN to draft ahead." }, + + # Inter-practice dependencies + { from = "htmx-rendering-profile", to = "config-driven-routes", kind = 'DependsOn, weight = 'High }, + { from = "bilingual-content-system", to = "config-driven-routes", kind = 'DependsOn, weight = 'Medium }, + { from = "layered-customization", to = "htmx-rendering-profile", kind = 'Complements, weight = 'Medium }, + + # Framework-vs-one-site tension → the seam that holds it + { from = "framework-vs-one-site", to = "customization-seam", kind = 'Resolves, weight = 'High, + note = "The seam holds the tension: per-consumer customization on the runtime side, identity and routes build-baked, neither pole collapsed." }, + { from = "consume-not-fork", to = "customization-seam", kind = 'Complements, weight = 'Medium, + note = "Customizing at the right layer is how a consumer avoids forking the framework." }, + + ], +} diff --git a/.ontoref/ontology/gate.ncl b/.ontoref/ontology/gate.ncl new file mode 100644 index 0000000..8cc2015 --- /dev/null +++ b/.ontoref/ontology/gate.ncl @@ -0,0 +1,83 @@ +let d = import "defaults/gate.ncl" in + +{ + membranes = [ + + d.make_membrane { + id = "framework-boundary", + name = "Framework Boundary Protection", + description = "Prevents this implementation from modifying Rustelo framework internals. All customization must go through the layered override system or the plugin architecture. Framework crates are dependencies, never edited in place.", + permeability = 'Closed, + accepts = ['FrameBreakingQuestion], + protects = ["framework upgrade path", "implementation independence", "override system integrity"], + opening_condition = { + max_tension_dimensions = 0, + pending_transitions = 0, + core_stable = true, + description = "Never opens — the consume-not-fork axiom is non-negotiable. Any framework change belongs upstream in the rustelo project.", + }, + closing_condition = "Direct modification to a rustelo framework crate, or a vendored copy of framework source, detected in this project.", + max_duration = 'Indefinite, + protocol = 'Reject, + active = true, + }, + + d.make_membrane { + id = "bilingual-parity-gate", + name = "Bilingual Parity Gate", + description = "Challenges any route or content addition that ships EN without a tracked path to ES parity. Blocks production publication of asymmetric routes.", + permeability = 'Low, + accepts = ['DepthDemonstrated, 'ProductiveMisunderstanding], + protects = ["bilingual parity invariant", "audience trust", "SEO hreflang correctness"], + opening_condition = { + max_tension_dimensions = 1, + pending_transitions = 2, + core_stable = true, + description = "Opens for EN-ahead drafting when the ES gap is registered as a publication blocker in content-coverage; never opens for production publication of an asymmetric route.", + }, + closing_condition = "A route or FTL key present in one locale and missing in the other reaches production publication.", + max_duration = 'Indefinite, + protocol = 'Challenge, + active = true, + }, + + d.make_membrane { + id = "config-driven-gate", + name = "Configuration-Driven Gate", + description = "Challenges any hardcoded language, path, route, or content type in Rust source. All such values must come from NCL/TOML configuration under site/config.", + permeability = 'Low, + accepts = ['ProductiveMisunderstanding, 'DepthDemonstrated], + protects = ["config-driven architecture", "language agnosticism", "deployment portability"], + opening_condition = { + max_tension_dimensions = 1, + pending_transitions = 2, + core_stable = true, + description = "Opens only when a literal is justified as a framework-provided default overridable via configuration.", + }, + closing_condition = "Hardcoded language code, route path, or content type found in implementation source without a configuration override.", + max_duration = 'Indefinite, + protocol = 'Challenge, + active = true, + }, + + d.make_membrane { + id = "purpose-gate", + name = "Purpose Alignment Gate", + description = "Challenges production publication when purpose.ncl criteria are not met: bilingual completeness, hydration parity on hydration-opted routes, and Hard SEO criteria (hreflang, JSON-LD, meta descriptions). Blocks any claim that the site is publication-ready unless validate-purpose-alignment passes all Hard criteria.", + permeability = 'Low, + accepts = ['DepthDemonstrated, 'FrameBreakingQuestion], + protects = ["purpose declaration accuracy", "bilingual parity at publication", "audience trust"], + opening_condition = { + max_tension_dimensions = 1, + pending_transitions = 2, + core_stable = true, + description = "Opens when validate-purpose-alignment passes all Hard criteria and publication-readiness has reached production-ready.", + }, + closing_condition = "A Hard validation or SEO criterion fails, or documentation/state describes the site as production-ready while content-coverage is below full-parity.", + max_duration = 'Indefinite, + protocol = 'Challenge, + active = true, + }, + + ], +} diff --git a/.ontoref/ontology/manifest.ncl b/.ontoref/ontology/manifest.ncl new file mode 100644 index 0000000..011804e --- /dev/null +++ b/.ontoref/ontology/manifest.ncl @@ -0,0 +1,74 @@ +# .ontoref/ontology/manifest.ncl — Project: website-htmx-rustelo +# Implementation of the Rustelo framework. repo_kind = 'RusteloApp activates the +# rustelo domain extension (ontoref rustelo layers|plugins|routes|wasm). + +let m = import "defaults/manifest.ncl" in + +m.make_manifest { + project = "website-htmx-rustelo", + repo_kind = 'RusteloApp, + description = "Bilingual EN/ES website implementing the Rustelo framework through the HTMX server-side rendering profile (ADR-006) with Minijinja templates. Routes, menus, themes and content types are configuration-driven (NCL/TOML); customization happens via Rustelo's layered override system, never by forking the framework.", + + consumption_modes = [ + m.make_consumption_mode { + consumer = 'EndUser, + needs = ['OntologyExport], + audit_level = 'Quick, + description = "Served website: visitors consume rendered bilingual pages. No ontology access — this is the deployed product surface.", + }, + m.make_consumption_mode { + consumer = 'Agent, + needs = ['OntologyExport], + audit_level = 'Standard, + description = "Reads .ontoref/ontology/core.ncl and the rustelo domain schema before modifying site config, content, or crates — to stay within framework PAP constraints.", + }, + ], + + layers = [ + m.make_layer { + id = "site-config", + paths = ["site/config/"], + committed = true, + description = "Configuration-driven surface: routes, menus, themes, rendering profile selection. NCL/TOML, no hardcoding.", + }, + m.make_layer { + id = "content", + paths = ["site/content/", "site/i18n/"], + committed = true, + description = "Bilingual content (en, es) and Fluent FTL translation bundles.", + }, + m.make_layer { + id = "crates", + paths = ["crates/"], + committed = true, + description = "Implementation crates consuming rustelo framework crates: server, client, pages, pages_htmx, shared, build-config.", + }, + m.make_layer { + id = "self-description", + paths = [".ontoref/", ".rustelo.ontoref/"], + committed = true, + description = "Ontoref self-description (ontology, ADRs, reflection) and the rustelo domain implementation declaration.", + }, + m.make_layer { + id = "process", + paths = [".coder/"], + committed = false, + description = "Session artifacts: plans, investigations, summaries. Process memory for actors.", + }, + ], + + operational_modes = [ + m.make_op_mode { + id = "dev", + description = "Standard implementation development: site config, content, crates.", + visible_layers = ["site-config", "content", "crates", "self-description"], + audit_level = 'Standard, + }, + m.make_op_mode { + id = "publish", + description = "Publication readiness: validate bilingual completeness, SEO criteria, hydration parity before deployment.", + visible_layers = ["site-config", "content", "self-description"], + audit_level = 'Standard, + }, + ], +} diff --git a/.ontoref/ontology/purpose-constraints.ncl b/.ontoref/ontology/purpose-constraints.ncl new file mode 100644 index 0000000..e16396e --- /dev/null +++ b/.ontoref/ontology/purpose-constraints.ncl @@ -0,0 +1,120 @@ +# Validation contracts for purpose.ncl files. +# Applied AFTER the schema contract — enforces semantic invariants beyond structural typing. +# Pattern mirrors adrs/adr-constraints.ncl: separate file, pure contracts, named exports. + +let _non_empty_objectives = std.contract.custom ( + fun label => + fun value => + if std.array.length value.content_objectives == 0 then + 'Error { + message = "purpose.ncl: content_objectives must not be empty — a purpose with no objectives is passive aspiration, not an executable intent" + } + else + 'Ok value +) in + +let _at_least_one_hard_criterion = std.contract.custom ( + fun label => + fun value => + let hard = value.validation_criteria + |> std.array.filter (fun vc => vc.severity == 'Hard) + in + if std.array.length hard == 0 then + 'Error { + message = "purpose.ncl: at least one ValidationCriteria must have severity='Hard — a purpose with only Soft criteria has no real publication gate" + } + else + 'Ok value +) in + +let _at_least_one_hard_seo_criterion = std.contract.custom ( + fun label => + fun value => + let hard = value.seo_criteria + |> std.array.filter (fun sc => sc.severity == 'Hard) + in + if std.array.length hard == 0 then + 'Error { + message = "purpose.ncl: at least one SeoCriterion must have severity='Hard — SEO without a hard criterion is aspirational, not enforceable" + } + else + 'Ok value +) in + +let _objective_audience_coverage = std.contract.custom ( + fun label => + fun value => + let declared = value.declaration.primary_audience in + let covered = value.content_objectives + |> std.array.flat_map (fun obj => obj.target_audience) + in + let uncovered = declared + |> std.array.filter (fun aud => + !(covered |> std.array.any (fun cov => cov == aud)) + ) + |> std.array.map (fun aud => std.to_string aud) + in + if std.array.length uncovered > 0 then + 'Error { + message = "purpose.ncl: audiences declared in declaration.primary_audience but not covered by any ContentObjective.target_audience: %{std.string.join ", " uncovered}" + } + else + 'Ok value +) in + +# Cross-field: every ValidationCriteria.applies_to id must reference an existing ContentObjective.id +let _valid_applies_to = std.contract.custom ( + fun label => + fun value => + let obj_ids = value.content_objectives |> std.array.map (fun o => o.id) in + let bad_refs = value.validation_criteria + |> std.array.flat_map (fun vc => + vc.applies_to + |> std.array.filter (fun ref_id => + !(obj_ids |> std.array.any (fun oid => oid == ref_id)) + ) + |> std.array.map (fun bad_id => + "criterion '%{vc.id}' applies_to unknown objective '%{bad_id}'" + ) + ) + in + if std.array.length bad_refs > 0 then + std.contract.blame_with_message + "purpose.ncl validation_criteria has invalid applies_to: %{std.string.join ", " bad_refs}" + label + else + 'Ok value +) in + +# Cross-field: every SeoCriterion.applies_to id must reference an existing ContentObjective.id +let _seo_applies_to = std.contract.custom ( + fun label => + fun value => + let obj_ids = value.content_objectives |> std.array.map (fun o => o.id) in + let bad_refs = value.seo_criteria + |> std.array.flat_map (fun sc => + sc.applies_to + |> std.array.filter (fun ref_id => + !(obj_ids |> std.array.any (fun oid => oid == ref_id)) + ) + |> std.array.map (fun bad_id => + "seo criterion '%{sc.id}' applies_to unknown objective '%{bad_id}'" + ) + ) + in + if std.array.length bad_refs > 0 then + std.contract.blame_with_message + "purpose.ncl seo_criteria has invalid applies_to: %{std.string.join ", " bad_refs}" + label + else + 'Ok value +) in + +{ + NonEmptyObjectives = _non_empty_objectives, + AtLeastOneHardCriterion = _at_least_one_hard_criterion, + AtLeastOneHardSeoCriterion = _at_least_one_hard_seo_criterion, + ObjectiveAudienceCoverage = _objective_audience_coverage, + ValidAppliesTo = _valid_applies_to, + SeoAppliesTo = _seo_applies_to, +} diff --git a/.ontoref/ontology/purpose-defaults.ncl b/.ontoref/ontology/purpose-defaults.ncl new file mode 100644 index 0000000..7f48dee --- /dev/null +++ b/.ontoref/ontology/purpose-defaults.ncl @@ -0,0 +1,28 @@ +let s = import "purpose-schema.ncl" in +let c = import "purpose-constraints.ncl" in + +# Factory for purpose.ncl files. +# Pattern mirrors adrs/adr-defaults.ncl: imports schema + constraints, exposes make_* factories. +# Cross-field constraints (ValidAppliesTo, SeoAppliesTo, ObjectiveAudienceCoverage) are applied +# here after schema validation so all fields are visible in the same record — same pattern as +# RequiresJustificationWhenRisky in adr-defaults.ncl. + +{ + make_purpose = fun data => + let result | s.PurposeFile = data in + let result | c.NonEmptyObjectives = result in + let result | c.AtLeastOneHardCriterion = result in + let result | c.AtLeastOneHardSeoCriterion = result in + let result | c.ObjectiveAudienceCoverage = result in + let result | c.ValidAppliesTo = result in + let result | c.SeoAppliesTo = result in + result, + + PurposeFile = s.PurposeFile, + PurposeDeclaration = s.PurposeDeclaration, + PublicationIntent = s.PublicationIntent, + ContentObjective = s.ContentObjective, + ValidationCriteria = s.ValidationCriteria, + SeoCriterion = s.SeoCriterion, + PropagationChannel = s.PropagationChannel, +} diff --git a/.ontoref/ontology/purpose-schema.ncl b/.ontoref/ontology/purpose-schema.ncl new file mode 100644 index 0000000..7ebdc18 --- /dev/null +++ b/.ontoref/ontology/purpose-schema.ncl @@ -0,0 +1,98 @@ +let _horizon_type = [| 'Days, 'Weeks, 'Months, 'Quarters, 'Years, 'Ongoing |] in +let _pole_type = [| 'Yang, 'Yin, 'Spiral |] in +let _audience_type = [| 'Developer, 'EndUser, 'Contributor, 'Agent, 'PublicInternet |] in +let _channel_type = [| 'GitRepo, 'PublishedSite, 'RSSFeed, 'Newsletter, 'SocialFeed, 'CLI, 'Docs |] in +let _seo_intent_type = [| 'Organic, 'Reference, 'Community, 'None |] in +let _severity_type = [| 'Hard, 'Soft |] in +let _channel_status = [| 'Active, 'Planned, 'Paused, 'Retired |] in + +let _purpose_declaration_type = { + id | String, + name | String, + pole | _pole_type, + statement | String, + elaboration | String, + primary_audience | Array _audience_type, + anti_purpose | String, + horizon | _horizon_type, +} in + +let _publication_intent_type = { + id | String, + name | String, + description | String, + channels | Array _channel_type, + readiness_gate | String, + trigger_condition | String, + anti_patterns | Array String, +} in + +let _content_objective_type = { + id | String, + name | String, + description | String, + target_audience | Array _audience_type, + content_kinds | Array String, + purpose_alignment_note | String, + seo_intent | _seo_intent_type, + horizon | _horizon_type, + active | Bool | default = true, +} in + +let _validation_criteria_type = { + id | String, + name | String, + description | String, + check_hint | String, + severity | _severity_type, + applies_to | Array String, +} in + +let _seo_criterion_type = { + id | String, + name | String, + description | String, + rationale | String, + check_hint | String, + severity | _severity_type, + applies_to | Array String, +} in + +let _propagation_channel_type = { + id | String, + name | String, + channel | _channel_type, + description | String, + status | _channel_status, + target_audience | Array _audience_type, + content_kinds | Array String, + seo_role | _seo_intent_type | default = 'None, + frequency | String | optional, + gate_id | String | optional, +} in + +let _purpose_file_type = { + declaration | _purpose_declaration_type, + publication_intent | _publication_intent_type, + content_objectives | Array _content_objective_type, + validation_criteria | Array _validation_criteria_type, + seo_criteria | Array _seo_criterion_type, + propagation_channels | Array _propagation_channel_type, +} in + +{ + Horizon = _horizon_type, + Pole = _pole_type, + Audience = _audience_type, + Channel = _channel_type, + SeoIntent = _seo_intent_type, + Severity = _severity_type, + ChannelStatus = _channel_status, + PurposeDeclaration = _purpose_declaration_type, + PublicationIntent = _publication_intent_type, + ContentObjective = _content_objective_type, + ValidationCriteria = _validation_criteria_type, + SeoCriterion = _seo_criterion_type, + PropagationChannel = _propagation_channel_type, + PurposeFile = _purpose_file_type, +} diff --git a/.ontoref/ontology/purpose.ncl b/.ontoref/ontology/purpose.ncl new file mode 100644 index 0000000..e2ef151 --- /dev/null +++ b/.ontoref/ontology/purpose.ncl @@ -0,0 +1,146 @@ +let p = import "purpose-defaults.ncl" in + +p.make_purpose { + + declaration = { + id = "website-htmx-rustelo-purpose", + name = "Website (HTMX/Rustelo) Purpose", + pole = 'Yin, + statement = "This site exists to publish bilingual EN/ES technical content through a Rustelo HTMX implementation, giving both language audiences the same depth of material with crawlable, server-rendered pages.", + elaboration = "The bilingual constraint is not cosmetic — it commits the site to closing the quality-technical-content gap for Spanish-speaking developers. The HTMX rendering profile is chosen for simplicity and crawlability; Leptos hydration remains available per-route. The purpose is fulfilled when both audiences (EN and ES) reach parity in content and in rendered quality, and the implementation never forks the Rustelo framework to get there.", + primary_audience = ['Developer, 'EndUser], + anti_purpose = "This site is not a marketing funnel, not a general-opinion blog, and not a lead-capture portfolio. It is not intended to maximize traffic at the cost of content depth, and it must never ship a route in one language only.", + horizon = 'Ongoing, + }, + + publication_intent = { + id = "website-htmx-rustelo-publication-intent", + name = "Publication Intent", + description = "Publishing means deploying to production where both EN and ES audiences can reach every launched content section. Publication requires bilingual completeness on launched sections, hydration parity for any hydration-opted route, and verified purpose-aligned SEO criteria.", + channels = ['PublishedSite, 'RSSFeed], + readiness_gate = "purpose-gate", + trigger_condition = "content-coverage is at full-parity, rendering-mode hydration-opted routes pass validate-hydration, and publication-readiness has reached production-ready. validate-purpose-alignment passes all Hard criteria.", + anti_patterns = [ + "Launching a content section with only one language populated", + "Publishing while publication-readiness is still pre-production", + "Publishing with known hydration mismatches on hydration-opted routes", + "Publishing content that serves no declared audience (Developer or EndUser)", + ], + }, + + content_objectives = [ + { + id = "bilingual-content", + name = "Bilingual Technical Content", + description = "Technical articles and pages published in both English and Spanish covering Rust, web architecture, and infrastructure. Bilingual parity is non-negotiable — every EN item has its ES equivalent and vice versa.", + target_audience = ['Developer, 'EndUser], + content_kinds = ["blog", "pages"], + purpose_alignment_note = "Primary vehicle of the purpose: closes the quality-technical-content gap for Spanish-speaking developers on the same stack.", + seo_intent = 'Organic, + horizon = 'Ongoing, + active = true, + }, + { + id = "project-showcase", + name = "Project Showcase", + description = "Bilingual presentation of technical projects with architecture, real status, and goals. Published status must reflect reality, not the desired state.", + target_audience = ['Developer, 'EndUser], + content_kinds = ["projects"], + purpose_alignment_note = "Demonstrates technical work with depth and honesty — reinforces the site as a knowledge platform, not marketing.", + seo_intent = 'Reference, + horizon = 'Months, + active = true, + }, + ], + + validation_criteria = [ + { + id = "bilingual-completeness", + name = "Bilingual Content Completeness Before Publication", + description = "No menu-visible section may publish if EN and ES counts diverge. Every menu-enabled route must have corresponding content in both language directories.", + check_hint = "Run the validate-content mode. The diff-routes and count-content steps must report zero asymmetries for menu-enabled routes.", + severity = 'Hard, + applies_to = ["bilingual-content", "project-showcase"], + }, + { + id = "purpose-audience-relevance", + name = "Content Serves Declared Audience", + description = "Every published content item must classify as serving the 'Developer or 'EndUser audience. Content serving no declared audience is a purpose-alignment violation.", + check_hint = "Review each content item's front-matter for audience markers; flag items with no audience classification. Run validate-purpose-alignment.", + severity = 'Hard, + applies_to = ["bilingual-content", "project-showcase"], + }, + { + id = "no-publish-during-hydration-instability", + name = "No Publication During Hydration Instability", + description = "The site must not deploy to production while any hydration-opted route fails hydration parity — rendering-mode must not regress below its current htmx-ssr baseline for served routes.", + check_hint = "Run validate-hydration: no empty HashMap in *Client components, SsrTranslator used, FTL registry populated for hydration-opted routes.", + severity = 'Hard, + applies_to = ["bilingual-content", "project-showcase"], + }, + ], + + seo_criteria = [ + { + id = "bilingual-canonical-urls", + name = "Bilingual Canonical URLs with hreflang", + description = "Every EN URL must carry to its ES equivalent and vice versa. The XML sitemap must include both variants for each page.", + rationale = "Serves the purpose directly: EN and ES audiences must find the same content in their language via organic search. Without hreflang, search engines may index only one variant.", + check_hint = "Audit a rendered blog route's HTML: confirm in , and that sitemap.xml includes both URLs.", + severity = 'Hard, + applies_to = ["bilingual-content", "project-showcase"], + }, + { + id = "structured-data-articles", + name = "Article Structured Data (JSON-LD)", + description = "Technical blog articles must include JSON-LD typed Article or TechArticle: author, datePublished, headline, inLanguage.", + rationale = "Improves prominence for developers searching technical guides — serves the purpose by connecting content to technical audiences through rich results.", + check_hint = "Inspect a rendered article's HTML for