init repo
This commit is contained in:
commit
3ccd15a14b
32 changed files with 2001 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
.coder/
|
||||
.claude/
|
||||
.internal.git/
|
||||
logs
|
||||
logs-archive
|
||||
.wrks
|
||||
.DS_Store
|
||||
|
||||
resources
|
||||
examples
|
||||
code
|
||||
desktop
|
||||
outreach
|
||||
vault
|
||||
|
||||
node_modules
|
||||
97
.ontoref/adrs/adr-001-single-image-deploy.ncl
Normal file
97
.ontoref/adrs/adr-001-single-image-deploy.ncl
Normal file
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
25
.ontoref/card.ncl
Normal file
25
.ontoref/card.ncl
Normal file
|
|
@ -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 = "",
|
||||
}
|
||||
39
.ontoref/config.ncl
Normal file
39
.ontoref/config.ncl
Normal file
|
|
@ -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",
|
||||
}
|
||||
30
.ontoref/ontology/connections.ncl
Normal file
30
.ontoref/ontology/connections.ncl
Normal file
|
|
@ -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
|
||||
139
.ontoref/ontology/core.ncl
Normal file
139
.ontoref/ontology/core.ncl
Normal file
|
|
@ -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." },
|
||||
|
||||
],
|
||||
}
|
||||
83
.ontoref/ontology/gate.ncl
Normal file
83
.ontoref/ontology/gate.ncl
Normal file
|
|
@ -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,
|
||||
},
|
||||
|
||||
],
|
||||
}
|
||||
74
.ontoref/ontology/manifest.ncl
Normal file
74
.ontoref/ontology/manifest.ncl
Normal file
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
}
|
||||
120
.ontoref/ontology/purpose-constraints.ncl
Normal file
120
.ontoref/ontology/purpose-constraints.ncl
Normal file
|
|
@ -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,
|
||||
}
|
||||
28
.ontoref/ontology/purpose-defaults.ncl
Normal file
28
.ontoref/ontology/purpose-defaults.ncl
Normal file
|
|
@ -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,
|
||||
}
|
||||
98
.ontoref/ontology/purpose-schema.ncl
Normal file
98
.ontoref/ontology/purpose-schema.ncl
Normal file
|
|
@ -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,
|
||||
}
|
||||
146
.ontoref/ontology/purpose.ncl
Normal file
146
.ontoref/ontology/purpose.ncl
Normal file
|
|
@ -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 <link rel=\"alternate\" hreflang=\"es\"> 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 <link rel=\"alternate\" hreflang=\"es|en\"> in <head>, 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 <script type=\"application/ld+json\"> with @type Article or TechArticle.",
|
||||
severity = 'Hard,
|
||||
applies_to = ["bilingual-content"],
|
||||
},
|
||||
{
|
||||
id = "meta-description-purpose-aligned",
|
||||
name = "Meta Descriptions Are Accurate and Purpose-Aligned",
|
||||
description = "Every published page must have a <meta name=\"description\"> describing the real content. No clickbait, no keyword stuffing.",
|
||||
rationale = "The purpose demands editorial honesty. A misleading meta description contradicts the anti_purpose ('not a marketing funnel').",
|
||||
check_hint = "Audit meta descriptions of the most-visited pages; verify each matches the real article content, not artificial keywords.",
|
||||
severity = 'Hard,
|
||||
applies_to = ["bilingual-content", "project-showcase"],
|
||||
},
|
||||
{
|
||||
id = "core-web-vitals-threshold",
|
||||
name = "Core Web Vitals Meet Threshold",
|
||||
description = "Lighthouse performance score >= 90 on mobile for main content pages. LCP < 2.5s, CLS < 0.1.",
|
||||
rationale = "A technical audience judges quality by performance; the HTMX SSR profile should make this readily achievable. Core Web Vitals are also a ranking factor.",
|
||||
check_hint = "Run Lighthouse (mobile) for /, and main content routes; verify score >= 90 and LCP/CLS within thresholds.",
|
||||
severity = 'Soft,
|
||||
applies_to = ["bilingual-content", "project-showcase"],
|
||||
},
|
||||
],
|
||||
|
||||
propagation_channels = [
|
||||
{
|
||||
id = "published-site",
|
||||
name = "Production Site",
|
||||
channel = 'PublishedSite,
|
||||
description = "Primary distribution channel: HTMX server-rendered pages serving all content kinds in EN and ES, with optional per-route Leptos hydration.",
|
||||
status = 'Active,
|
||||
target_audience = ['EndUser, 'Developer],
|
||||
content_kinds = ["blog", "pages", "projects"],
|
||||
seo_role = 'Organic,
|
||||
gate_id = "purpose-gate",
|
||||
},
|
||||
{
|
||||
id = "rss-feed",
|
||||
name = "RSS Feed",
|
||||
channel = 'RSSFeed,
|
||||
description = "RSS feed for blog articles, enabling syndication and subscription-based consumption.",
|
||||
status = 'Planned,
|
||||
target_audience = ['Developer, 'EndUser],
|
||||
content_kinds = ["blog"],
|
||||
seo_role = 'None,
|
||||
frequency = "on every new blog post",
|
||||
},
|
||||
],
|
||||
}
|
||||
37
.ontoref/ontology/schemas/project-card.ncl
Normal file
37
.ontoref/ontology/schemas/project-card.ncl
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Project card schema — typed self-definition for any project.
|
||||
# Source of truth for display metadata, web assets, and portfolio publication.
|
||||
#
|
||||
# Each project maintains card.ncl locally and publishes (copies) to the
|
||||
# portfolio repo alongside its assets/. The portfolio is self-contained —
|
||||
# it does not depend on the original project repo being alive.
|
||||
|
||||
let source_type = [| 'Local, 'Remote, 'Historical |] in
|
||||
|
||||
let project_pub_status_type = [| 'Active, 'Beta, 'Maintenance, 'Archived, 'Stealth |] in
|
||||
|
||||
let project_card_type = {
|
||||
id | String, # matches ontology_node in jpl DAG
|
||||
name | String,
|
||||
tagline | String, # stable identity statement
|
||||
description | String,
|
||||
primary_value_prop_id | String | optional, # ADR-035: canonical value-prop in .ontoref/positioning/value-props/; downstream renderers MAY prefer its claim over `tagline`
|
||||
version | String | default = "",
|
||||
status | project_pub_status_type,
|
||||
source | source_type | default = 'Local,
|
||||
url | String | default = "",
|
||||
repo | String | default = "",
|
||||
docs | String | default = "",
|
||||
logo | String | default = "",
|
||||
started_at | String | default = "",
|
||||
tags | Array String | default = [],
|
||||
tools | Array String | default = [],
|
||||
features | Array String | default = [],
|
||||
featured | Bool | default = false,
|
||||
sort_order | Number | default = 0,
|
||||
} in
|
||||
|
||||
{
|
||||
SourceType = source_type,
|
||||
ProjectPubStatus = project_pub_status_type,
|
||||
ProjectCard = project_card_type,
|
||||
}
|
||||
139
.ontoref/ontology/state.ncl
Normal file
139
.ontoref/ontology/state.ncl
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
let d = import "defaults/state.ncl" in
|
||||
|
||||
{
|
||||
dimensions = [
|
||||
|
||||
d.make_dimension {
|
||||
id = "content-coverage",
|
||||
name = "Bilingual Content Coverage",
|
||||
description = "Parity between EN and ES content across all public routes and FTL bundles.",
|
||||
current_state = "en-ahead",
|
||||
desired_state = "full-parity",
|
||||
horizon = 'Weeks,
|
||||
states = [
|
||||
d.make_state {
|
||||
id = "en-ahead",
|
||||
name = "English Ahead",
|
||||
description = "EN content drafted for most routes; ES lags on a subset. Tracked as publication blockers, not invariant violations.",
|
||||
tension = 'Medium,
|
||||
},
|
||||
d.make_state {
|
||||
id = "full-parity",
|
||||
name = "Full Parity",
|
||||
description = "Every public route and FTL key exists in both EN and ES. validate-content reports zero asymmetries.",
|
||||
tension = 'Low,
|
||||
},
|
||||
],
|
||||
transitions = [
|
||||
d.make_transition {
|
||||
from = "en-ahead",
|
||||
to = "full-parity",
|
||||
condition = "validate-content mode reports route symmetry, content-file completeness, and FTL key coverage with zero gaps for ES.",
|
||||
catalyst = "Site config and content directories established; validate-content mode available from the rustelo implementation declaration.",
|
||||
blocker = "ES content and FTL keys not yet complete for all routes.",
|
||||
horizon = 'Weeks,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
d.make_dimension {
|
||||
id = "rendering-mode",
|
||||
name = "Rendering Mode Maturity",
|
||||
description = "Maturity of the HTMX server-side rendering profile and optional Leptos hydration opt-in.",
|
||||
current_state = "htmx-ssr",
|
||||
desired_state = "dual-mode-verified",
|
||||
horizon = 'Months,
|
||||
states = [
|
||||
d.make_state {
|
||||
id = "htmx-ssr",
|
||||
name = "HTMX SSR Default",
|
||||
description = "All routes render via the HTMX profile with Minijinja templates and fragment swaps. Stable.",
|
||||
tension = 'Low,
|
||||
},
|
||||
d.make_state {
|
||||
id = "dual-mode-verified",
|
||||
name = "Dual-Mode Verified",
|
||||
description = "Selected routes opt into Leptos hydration with verified SSR/WASM parity (validate-hydration passes) alongside the HTMX default.",
|
||||
tension = 'Medium,
|
||||
},
|
||||
],
|
||||
transitions = [
|
||||
d.make_transition {
|
||||
from = "htmx-ssr",
|
||||
to = "dual-mode-verified",
|
||||
condition = "validate-hydration mode passes for hydration-opted routes: no empty HashMap in *Client components, SsrTranslator used, FTL registry populated.",
|
||||
catalyst = "Rustelo rendering-profile axis (ADR-006) supports per-route profile selection.",
|
||||
blocker = "No routes opted into hydration yet; hydration parity not exercised in this implementation.",
|
||||
horizon = 'Months,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
d.make_dimension {
|
||||
id = "publication-readiness",
|
||||
name = "Publication Readiness",
|
||||
description = "Readiness of the site for production deployment: content parity, SEO criteria, hydration stability.",
|
||||
current_state = "pre-production",
|
||||
desired_state = "production-ready",
|
||||
horizon = 'Months,
|
||||
states = [
|
||||
d.make_state {
|
||||
id = "pre-production",
|
||||
name = "Pre-Production",
|
||||
description = "Site builds and serves bilingual content; publication blockers (ES parity, SEO) remain open.",
|
||||
tension = 'Medium,
|
||||
},
|
||||
d.make_state {
|
||||
id = "production-ready",
|
||||
name = "Production Ready",
|
||||
description = "validate-purpose-alignment passes all Hard criteria: bilingual completeness, hreflang/JSON-LD/meta, hydration stability.",
|
||||
tension = 'Low,
|
||||
},
|
||||
],
|
||||
transitions = [
|
||||
d.make_transition {
|
||||
from = "pre-production",
|
||||
to = "production-ready",
|
||||
condition = "plan-publication and validate-purpose-alignment modes report all Hard criteria satisfied; content-coverage at full-parity.",
|
||||
catalyst = "Reflection modes for content, hydration, and purpose alignment available via .rustelo.ontoref/.",
|
||||
blocker = "content-coverage not at full-parity; SEO infrastructure (hreflang, JSON-LD, sitemap) not fully verified.",
|
||||
horizon = 'Months,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
d.make_dimension {
|
||||
id = "consumer-neutrality",
|
||||
name = "Consumer Neutrality",
|
||||
description = "How much of a consumer site can be expressed without rebuilding the shared rustelo-htmx-server binary — whether the binary serves N consumers from consumer-supplied config or bakes one site in.",
|
||||
current_state = "neutral-framework",
|
||||
desired_state = "neutral-framework",
|
||||
horizon = 'Months,
|
||||
states = [
|
||||
d.make_state {
|
||||
id = "one-site-baked",
|
||||
name = "One Site Baked",
|
||||
description = "site/config (routes, menus, theme, logo, languages) is compiled into the binary, so the binary is one specific site. Brand, content, templates and env are already runtime per consumer, but identity and routes are not.",
|
||||
tension = 'Medium,
|
||||
},
|
||||
d.make_state {
|
||||
id = "neutral-framework",
|
||||
name = "Neutral Framework (single image)",
|
||||
description = "ONE shared binary/image serves N consumers (decision 2026-06-14: sites differ in content/brand, not route structure). The route table is a baked SHARED SUPERSET (structure, kept compiled — performance, rustelo's grain); per-site SUBSTANCE — brand, content, templates, origin, logo, name, languages — is supplied at deploy via runtime config/env, no rebuild. A verifier rejects brand/content leaking into crates/server/src.",
|
||||
tension = 'Low,
|
||||
},
|
||||
],
|
||||
transitions = [
|
||||
d.make_transition {
|
||||
from = "one-site-baked",
|
||||
to = "neutral-framework",
|
||||
condition = "REACHED (2026-06-14). validate-contract green (seam · route-superset · neutral-binary · config-drift) AND every per-site SUBSTANCE knob is runtime, leaving only the SHARED route superset baked.",
|
||||
catalyst = "ADR-001 (single-image deploy). All per-site substance is runtime: origin (SITE_BASE_URL env), name (SITE_NAME env), logo (consumer site.ncl [logo], read at runtime by theme::load_logo_config → site_logo global → nav/footer/home — config-driven, no env, no template hardcode), languages (SUPPORTED_LANGUAGES), brand CSS (custom.css), templates (HTMX_TEMPLATE_PATH), FTL (deferred), menus/footer (routes.ncl/footer.ncl at startup), the level-aware About head. Four hard-gate verifiers (`just validate-contract`) enforce seam, superset completeness, neutral-binary and consumer⊆superset. Verified with two consumers on one binary: ontoref + vapora each show their OWN logo from their OWN site.ncl, no rebuild.",
|
||||
blocker = "None — GRADUATED 2026-06-14. Second independent consumer proven: vapora (vapora/outreach/site) served on the SHARED binary from its own dir with its own brand (vapora-logo, SITE_NAME=vapora, vapora.dev) and its OWN menu ('About Vapora' — proves manifest_config_path resolves cwd-relative to the consumer's routes.ncl, no hidden coupling), no rebuild. sufficient-verification's second realized instance is met. Residual polish only: the baked superset still carries this repo's portfolio routes (harmless extras); a real vapora deploy supplies its own content/FTL (the test reused ontoref's as skeleton).",
|
||||
horizon = 'Months,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
}
|
||||
14
.ontoref/project.ncl
Normal file
14
.ontoref/project.ncl
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
let s = import "ontoref-project.ncl" in
|
||||
|
||||
s.make_project {
|
||||
slug = "website-htmx-rustelo",
|
||||
root = "/Users/Akasha/Development/website-htmx-rustelo",
|
||||
nickel_import_paths = [
|
||||
"/Users/Akasha/Development/website-htmx-rustelo",
|
||||
"/Users/Akasha/Development/website-htmx-rustelo/.ontoref/ontology",
|
||||
"/Users/Akasha/Development/website-htmx-rustelo/.ontoref/adrs",
|
||||
"/Users/Akasha/Development/website-htmx-rustelo/.ontoref/reflection/schemas",
|
||||
"/Users/Akasha/Development/ontoref/code",
|
||||
"/Users/Akasha/Development/ontoref/code/ontology",
|
||||
],
|
||||
}
|
||||
51
.ontoref/reflection/constraints.ncl
Normal file
51
.ontoref/reflection/constraints.ncl
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Validation contracts for reflection modes.
|
||||
# These are applied AFTER the schema contract — they enforce semantic invariants
|
||||
# that Nickel's structural typing cannot express alone.
|
||||
# Pattern mirrors jpl_ontology/adrs/constraints.ncl: separate file, pure contracts.
|
||||
|
||||
let _non_empty_steps = std.contract.custom (
|
||||
fun label =>
|
||||
fun value =>
|
||||
if std.array.length value.steps == 0 then
|
||||
'Error {
|
||||
message = "Mode '%{value.id}': steps must not be empty — a mode with no steps is passive documentation, not an executable contract"
|
||||
}
|
||||
else
|
||||
'Ok value
|
||||
) in
|
||||
|
||||
let _valid_trigger = std.contract.custom (
|
||||
fun label =>
|
||||
fun value =>
|
||||
if std.string.length value.trigger == 0 then
|
||||
'Error {
|
||||
message = "Mode '%{value.id}': trigger must not be empty — it identifies how this mode is invoked"
|
||||
}
|
||||
else
|
||||
'Ok value
|
||||
) in
|
||||
|
||||
# Ensures every step that declares a cmd actually has a meaningful command (not whitespace-only).
|
||||
let _non_empty_cmds = std.contract.custom (
|
||||
fun label =>
|
||||
fun value =>
|
||||
let bad = value.steps
|
||||
|> std.array.filter (fun s =>
|
||||
std.record.has_field "cmd" s
|
||||
&& std.string.length (std.string.trim s.cmd) == 0
|
||||
)
|
||||
|> std.array.map (fun s => s.id)
|
||||
in
|
||||
if std.array.length bad > 0 then
|
||||
'Error {
|
||||
message = "Mode '%{value.id}': steps with empty cmd: %{std.string.join ", " bad}"
|
||||
}
|
||||
else
|
||||
'Ok value
|
||||
) in
|
||||
|
||||
{
|
||||
NonEmptySteps = _non_empty_steps,
|
||||
ValidTrigger = _valid_trigger,
|
||||
NonEmptyCmds = _non_empty_cmds,
|
||||
}
|
||||
28
.ontoref/reflection/defaults.ncl
Normal file
28
.ontoref/reflection/defaults.ncl
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
let s = import "schema.ncl" in
|
||||
let c = import "constraints.ncl" in
|
||||
|
||||
# Factory functions for reflection types.
|
||||
# Pattern mirrors jpl_ontology/adrs/defaults.ncl: import schema + constraints, expose factories.
|
||||
|
||||
{
|
||||
# make_mode ActionContract data → validates structural contract + semantic constraints.
|
||||
# The Mode contract enforces DAG correctness (uniqueness + referential integrity).
|
||||
# Cycle detection runs separately in ontoref-reflection::dag::validate.
|
||||
make_mode = fun ActionContract => fun data =>
|
||||
let result | (s.Mode ActionContract) = data in
|
||||
let result | c.NonEmptySteps = result in
|
||||
let result | c.ValidTrigger = result in
|
||||
result,
|
||||
|
||||
# make_step ActionContract data → validates a single step against the schema.
|
||||
make_step = fun ActionContract => fun data =>
|
||||
data | (s.ActionStep ActionContract),
|
||||
|
||||
# Re-export raw types for consumers that apply contracts manually.
|
||||
Mode = s.Mode,
|
||||
ActionStep = s.ActionStep,
|
||||
Dependency = s.Dependency,
|
||||
OnError = s.OnError,
|
||||
Guard = s.Guard,
|
||||
Converge = s.Converge,
|
||||
}
|
||||
70
.ontoref/reflection/modes/plan-publication.ncl
Normal file
70
.ontoref/reflection/modes/plan-publication.ncl
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
d.make_mode String {
|
||||
id = "plan-publication",
|
||||
trigger = "Generate a content publication plan: which content sections need ES completion, what SEO criteria need implementation, and what state transitions must complete before production deployment — ordered by dependency.",
|
||||
|
||||
preconditions = [
|
||||
".ontoref/ontology/purpose.ncl exists and exports cleanly",
|
||||
".ontoref/ontology/state.ncl exists",
|
||||
"code/site/content/ exists or code/site/i18n/ exists",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "run-validate-purpose-alignment",
|
||||
action = "Execute the validate-purpose-alignment mode as a sub-check. All Hard criterion failures become required blockers in this plan; UNVERIFIED items become required verification tasks.",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "audit-content-coverage",
|
||||
action = "For each ContentObjective in purpose.ncl (bilingual-content, project-showcase): count EN and ES items in the corresponding content directory. Report sections where ES count < EN count with specific file counts.",
|
||||
cmd = "for lang in en es; do echo \"=== $lang ===\"; find code/site/content/$lang -name '*.md' 2>/dev/null | wc -l; done",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "run-validate-purpose-alignment" }],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "audit-state-blockers",
|
||||
action = "Read state.ncl content-coverage, rendering-mode, and publication-readiness dimensions. For each dimension not at its desired_state, list all blockers from transitions. These become plan items ordered by dependency: content-coverage must reach full-parity before publication-readiness can advance to production-ready.",
|
||||
cmd = "nickel export .ontoref/ontology/state.ncl",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "audit-content-coverage" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "audit-seo-implementation",
|
||||
action = "Check which SEO infrastructure is implemented in the server/page crates: (1) hreflang generation in the SSR head, (2) JSON-LD Article generation in blog post pages, (3) meta-description generation in page components, (4) sitemap.xml generation. Report each as IMPLEMENTED / MISSING / PARTIAL.",
|
||||
cmd = "rg 'hreflang|application/ld\\+json|meta.*description|sitemap' code/crates/ --type rust -l 2>/dev/null | head -20",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "run-validate-purpose-alignment" }],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "propose-plan",
|
||||
action = "Generate a concrete publication plan with items ordered by dependency: (1) SEO infrastructure gaps to implement (hreflang, JSON-LD, meta descriptions, sitemap) — technical prerequisites; (2) ES content items to author per section with specific file counts; (3) state transitions to complete in order: content-coverage → rendering-mode → publication-readiness. Each item names the specific Hard criterion it satisfies.",
|
||||
actor = 'Agent,
|
||||
depends_on = [
|
||||
{ step = "audit-state-blockers" },
|
||||
{ step = "audit-seo-implementation" },
|
||||
],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "review",
|
||||
action = "Human reviews and accepts the publication plan. The accepted plan becomes the authoritative backlog for content and SEO work. Hard blockers are prioritized over Soft items.",
|
||||
actor = 'Human,
|
||||
depends_on = [{ step = "propose-plan" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
|
||||
postconditions = [
|
||||
"Publication plan enumerates all ES content gaps with specific file counts per section",
|
||||
"Plan lists SEO infrastructure items as IMPLEMENTED / MISSING / PARTIAL",
|
||||
"Plan lists state transitions in dependency order (content-coverage → rendering-mode → publication-readiness)",
|
||||
"Plan distinguishes Hard blockers from Soft items",
|
||||
"Plan accepted by human reviewer",
|
||||
],
|
||||
}
|
||||
98
.ontoref/reflection/modes/publish-content.ncl
Normal file
98
.ontoref/reflection/modes/publish-content.ncl
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
# Consumption contract for publishing content INTO a Rustelo HTMX-SSR site.
|
||||
#
|
||||
# This mode exists because the site's self-description validates content QUALITY
|
||||
# (validate-content: bilingual parity; plan-publication: coverage/SEO/state) but
|
||||
# never declared the MECHANICS of getting a page to render. Those mechanics live
|
||||
# scattered in adr-001 (build-time codegen), adr-006 (htmx profile), and adr-007
|
||||
# (templates+generator). A consumer that assumes "drop a markdown file and it is
|
||||
# visible" is wrong by design: routes and content-kinds are build-time codegen.
|
||||
#
|
||||
# The build/runtime boundary — VERIFIED against the binary, not assumed:
|
||||
# - Content-TYPE discovery is RUNTIME. content_processor reads
|
||||
# site/content/content-kinds.toml ([[content_kinds]] name/enabled); if that
|
||||
# file is absent it falls back to [blog, recipes] (rustelo_server bin: "no
|
||||
# hardcoded types"). So enabling a new kind (projects, …) is a TOML edit →
|
||||
# NO recompile. A kind that is a silent no-op is simply missing from that TOML.
|
||||
# - Markdown WITHIN an enabled kind is indexed at runtime by content_processor
|
||||
# (→ site/r/<kind>/<lang>/) → NO recompile.
|
||||
# - Only ROUTES are build-time codegen (config/routes/*.ncl, adr-001): the server
|
||||
# must already carry the kind's <Handler> route. Adding a NEW route REQUIRES
|
||||
# `cargo build`. Reusing an existing routed kind (e.g. projects) does not.
|
||||
# - Serving path: the server reads processed content from public/r; if the site
|
||||
# uses it, sync site/r → site/public/r (just sync) after reindexing.
|
||||
#
|
||||
# Content layout per kind shape (from the living model):
|
||||
# - index-style (projects): content/<kind>/<lang>/<slug>/index.md → /<kind>/<slug>
|
||||
# - category-style (blog): content/<kind>/<lang>/<category>/<slug>.md → /<kind>/<category>/<slug>
|
||||
#
|
||||
# Site identity (logo/name/footer/nav) is NOT content — it lives in
|
||||
# site/config/site.ncl + templates/build-tools/partials/{header,footer}.tera.
|
||||
|
||||
d.make_mode String {
|
||||
id = "publish-content",
|
||||
trigger = "Publish content into a Rustelo HTMX-SSR site: place markdown in the content-kind's on-disk layout, reindex with content_processor (no recompile for an existing kind), and verify the route serves — honoring the build-time/runtime boundary (adr-001) and the bilingual-parity axiom (validate-content).",
|
||||
|
||||
preconditions = [
|
||||
"site/content/ exists",
|
||||
"content_processor is on PATH",
|
||||
"the target content-kind is listed in site/content/content-kinds.toml (runtime registry — add it if missing, no recompile)",
|
||||
"the target kind already has a <Handler> route in config/routes (build-time, adr-001 — a NEW route needs `cargo build`; reusing an existing routed kind like projects does not)",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "classify-target",
|
||||
action = "Confirm content_processor recognises the kind. Discovery is RUNTIME from site/content/content-kinds.toml — NOT compiled in. Oracle: run content_processor on the kind; 'Processing <kind>' means it is enabled. If it is a silent no-op, the kind is missing from content-kinds.toml — add a [[content_kinds]] entry (name=<kind>, enabled=true). This is a TOML edit, NO recompile. Only check the route separately: the kind needs a <Handler> route in config/routes, which is build-time (reusing an already-routed kind like projects avoids any build).",
|
||||
cmd = "content_processor --content-type \"$KIND\" 2>&1 | grep -qi \"processing $KIND\" && echo \"kind enabled (runtime)\" || echo \"kind missing from content-kinds.toml — add a [[content_kinds]] entry (no recompile)\"",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "place-content",
|
||||
action = "Write the markdown at the kind's layout: index-style kinds (e.g. projects) use content/<kind>/<lang>/<slug>/index.md and serve at /<kind>/<slug>; category-style kinds (e.g. blog) use content/<kind>/<lang>/<category>/<slug>.md. Author BOTH languages (en and es) — the bilingual-parity axiom makes a route present in one language only a hard error. Frontmatter must carry at least id, title, slug, subtitle, excerpt, author, date, published, category, tags.",
|
||||
actor = 'Agent,
|
||||
depends_on = [{ step = "classify-target" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "enforce-bilingual-parity",
|
||||
action = "Run the validate-content mode as a sub-check: EN/ES route symmetry, content-file count parity, FTL key parity. Any route present in one language but not the other is a blocking error against the bilingual-parity gate; resolve before reindexing.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "place-content" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
action = "Regenerate the runtime content index with content_processor (writes site/r/<kind>/<lang>/ + filter-index.json + category indexes). With the kind enabled in content-kinds.toml this is the SINGLE activation command — no recompile. Then ensure the server's serving path is current: if the site serves from public/r, sync site/r → site/public/r (just sync) so the new index is visible.",
|
||||
id = "reindex",
|
||||
cmd = "content_processor --content-type $KIND",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "enforce-bilingual-parity" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "verify-oracle",
|
||||
action = "Start the server (run.sh / rustelo-htmx-server) and GET each published route in both languages — /<kind>/<slug> and its localized path — asserting the page renders the authored title/body, not a 404 or the index fallback. This observed render is the oracle; publication is not done until it passes.",
|
||||
cmd = "for u in \"/$KIND/$SLUG\" \"/$KIND_ES/$SLUG\"; do printf '%s -> ' \"$u\"; curl -sfI \"http://localhost:3000$u\" >/dev/null && echo ok || echo FAIL; done",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "reindex" }],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "review",
|
||||
action = "Human confirms the rendered pages match intent in both languages. Only then is the content published. A failing oracle is a blocker, never bridged by assumption.",
|
||||
actor = 'Human,
|
||||
depends_on = [{ step = "verify-oracle" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
|
||||
postconditions = [
|
||||
"Content exists at the kind's layout in BOTH languages (bilingual-parity satisfied)",
|
||||
"The kind is enabled in content-kinds.toml (runtime registry) — a new TYPE costs a TOML edit, never a recompile",
|
||||
"content_processor index (site/r/<kind>) regenerated and synced to the server's serving path (public/r if used)",
|
||||
"Each published route returns the authored page in both languages (oracle passed)",
|
||||
"Only a NEW ROUTE is flagged as build-time (cargo build); enabling a content-type is never silently treated as needing a rebuild",
|
||||
],
|
||||
}
|
||||
44
.ontoref/reflection/modes/sync-ontology.ncl
Normal file
44
.ontoref/reflection/modes/sync-ontology.ncl
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
d.make_mode String {
|
||||
id = "sync-ontology",
|
||||
trigger = "Synchronize .ontoref/ontology/ declarations with actual project artifacts — detect drift between declared practice nodes and the crates/site structure under code/, and propose ontology patches.",
|
||||
|
||||
preconditions = [
|
||||
".ontoref/ontology/core.ncl exists",
|
||||
"nickel binary available on PATH",
|
||||
"rg (ripgrep) available on PATH",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "scan",
|
||||
action = "Enumerate the implementation structure: crates under code/crates/, config under code/site/config/, content directories, and the rendering profile declarations. Extract the artifact paths actually present.",
|
||||
cmd = "ls code/crates/ 2>/dev/null; echo '---'; ls code/site/config/ 2>/dev/null",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "export-core",
|
||||
action = "Export .ontoref/ontology/core.ncl and collect the artifact_paths declared on each practice node.",
|
||||
cmd = "nickel export .ontoref/ontology/core.ncl 2>/dev/null",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "scan" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "diff",
|
||||
action = "Compare scan results against core.ncl artifact_paths. Categorize each: OK (node path exists), STALE (declared path absent under code/), MISSING (significant artifact with no node). Account for the code/ prefix introduced by the constellation layout.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "export-core" }],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "propose",
|
||||
action = "Generate Nickel patches: corrected artifact_paths for STALE nodes, new Practice node stubs for MISSING artifacts. Present for human review; do not apply automatically.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "diff" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
}
|
||||
37
.ontoref/reflection/modes/validate-config-drift.ncl
Normal file
37
.ontoref/reflection/modes/validate-config-drift.ncl
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
# Verifier for consumer↔superset config drift (ADR-001, single-image deploy).
|
||||
# A consumer supplies its own routes.ncl (drives menus/footer at runtime), but the
|
||||
# servable routes are the SHARED SUPERSET baked into the binary. If the consumer
|
||||
# declares a route path absent from the superset, its menu/link 404s — the exact
|
||||
# build/runtime split gotcha. This checks: consumer route paths ⊆ superset paths.
|
||||
# Cross-repo (needs a consumer config dir); runnable as
|
||||
# `just validate-config-drift <consumer-site/config>` (defaults to self = zero drift).
|
||||
|
||||
d.make_mode String {
|
||||
id = "validate-config-drift",
|
||||
trigger = "Validate a consumer's route paths are a subset of the baked route superset so its menu/links never 404 against the shared binary. ADR-001 single-image deploy.",
|
||||
|
||||
preconditions = [
|
||||
"nickel and jq available on PATH",
|
||||
"code/site/config/routes.ncl exists (the superset)",
|
||||
"a consumer site/config dir with routes.ncl is provided (defaults to this repo's)",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "check-subset",
|
||||
action = "Export both routes.ncl to JSON, collect the en+es path strings of each, and report any path declared by the consumer that is NOT in the superset (those 404 on the shared binary). {slug} template routes compare as identical strings, so param routes match by shape.",
|
||||
cmd = "cd code && NICKEL={{ rustelo_root }}/resources/nickel; paths(){ nickel export --import-path \"$NICKEL\" --import-path \"$1\" \"$1/routes.ncl\" 2>/dev/null | jq -r '[.routes[].paths.en,.routes[].paths.es]|map(select(.))|.[]' 2>/dev/null | sort -u; }; sup=$(paths site/config); cons=$(paths \"${CONSUMER_CONFIG:-site/config}\"); comm -23 <(echo \"$cons\") <(echo \"$sup\") | sed 's/^/DRIFT /'; echo done",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "report",
|
||||
action = "No DRIFT lines ⇒ consumer routes ⊆ superset, every consumer link resolves. Any DRIFT path ⇒ either add it to the framework superset (site/config/routes.ncl + content.ncl + rebuild) or remove it from the consumer; until then that menu/link 404s.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "check-subset" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
}
|
||||
47
.ontoref/reflection/modes/validate-content.ncl
Normal file
47
.ontoref/reflection/modes/validate-content.ncl
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
d.make_mode String {
|
||||
id = "validate-content",
|
||||
trigger = "Validate bilingual content integrity: diff EN vs ES routes, verify content files exist for both languages, and check FTL translation-key parity. Enforces the bilingual-parity axiom.",
|
||||
|
||||
preconditions = [
|
||||
"code/site/config/routes/ contains language-specific route config files",
|
||||
"code/site/content/ exists",
|
||||
"rg (ripgrep) available on PATH",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "diff-routes",
|
||||
action = "Compare EN and ES route configurations under code/site/config/routes/. Extract route paths from both language config files and report asymmetries (routes present in one language but not the other).",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "count-content",
|
||||
action = "Count content files per language and report any language whose count trails the other, with the specific gap. These become content-coverage publication blockers.",
|
||||
cmd = "for lang in en es; do printf '%s: ' \"$lang\"; find code/site/content/$lang -name '*.md' 2>/dev/null | wc -l; done",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "diff-routes" }],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "diff-ftl-keys",
|
||||
action = "Compare Fluent FTL translation keys between EN and ES locales. Report keys present in one locale but missing in the other.",
|
||||
cmd = "diff <(rg '^[a-z]' code/site/i18n/locales/en/ -h --no-line-number 2>/dev/null | sort) <(rg '^[a-z]' code/site/i18n/locales/es/ -h --no-line-number 2>/dev/null | sort)",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "diff-routes" }],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "report",
|
||||
action = "Generate a content integrity report: route symmetry, content-file completeness, FTL key coverage. Classify missing content as warnings and missing routes as errors against the bilingual-parity gate.",
|
||||
actor = 'Both,
|
||||
depends_on = [
|
||||
{ step = "count-content" },
|
||||
{ step = "diff-ftl-keys" },
|
||||
],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
}
|
||||
46
.ontoref/reflection/modes/validate-hydration.ncl
Normal file
46
.ontoref/reflection/modes/validate-hydration.ncl
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
d.make_mode String {
|
||||
id = "validate-hydration",
|
||||
trigger = "Validate SSR/WASM hydration parity for routes that opt into the Leptos hydration profile: scan for empty HashMap in *Client components, verify SsrTranslator usage, and check FTL registry population. Guards the rendering-mode dimension.",
|
||||
|
||||
preconditions = [
|
||||
"code/crates/ exists",
|
||||
"rg (ripgrep) available on PATH",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "scan-empty-hashmap",
|
||||
action = "Scan for HashMap::new() in *Client page components. Any hit in a lang_content context is a hydration violation — the client must hydrate the same content the server rendered.",
|
||||
cmd = "rg 'HashMap::new\\(\\)' code/crates/ --type rust -g '*client*' -g '*Client*' --count 2>/dev/null || echo '0 — clean: no HashMap::new() in Client components'",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "verify-ssr-translator",
|
||||
action = "Verify *Client page components use SsrTranslator or build_page_content_patterns so translations resolve identically on server and client.",
|
||||
cmd = "rg 'SsrTranslator' code/crates/ --type rust --count 2>/dev/null || echo '0 — SsrTranslator not referenced yet'",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "verify-ftl-registry",
|
||||
action = "Verify the WASM FTL registry is populated during client initialization (populate_wasm_ftl_registry or equivalent) so the hydration path has the same locale bundles as SSR.",
|
||||
cmd = "rg 'populate_wasm_ftl_registry|wasm_ftl' code/crates/client/ 2>/dev/null || echo '— FTL registry hook not found (expected until a route opts into hydration)'",
|
||||
actor = 'Both,
|
||||
depends_on = [
|
||||
{ step = "scan-empty-hashmap" },
|
||||
{ step = "verify-ssr-translator" },
|
||||
],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "report",
|
||||
action = "Report hydration parity status. Any empty-HashMap hit in a Client component blocks the rendering-mode transition to dual-mode-verified. Since the site defaults to the HTMX profile, this mode is advisory until routes opt into hydration.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "verify-ftl-registry" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
}
|
||||
35
.ontoref/reflection/modes/validate-neutral-binary.ncl
Normal file
35
.ontoref/reflection/modes/validate-neutral-binary.ncl
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
# Verifier for binary neutrality (ADR-001, single-image deploy).
|
||||
# Every per-site identity knob — logo (light/dark/alt), public origin, site name —
|
||||
# must remain RUNTIME-OVERRIDABLE in source (an env var read with a baked default),
|
||||
# so the shared binary carries only DEFAULTS and a consumer overrides them at deploy.
|
||||
# A knob that loses its override would hardcode one site into the binary, breaking
|
||||
# the single-image model. Static source check; runnable as `just validate-neutral-binary`.
|
||||
|
||||
d.make_mode String {
|
||||
id = "validate-neutral-binary",
|
||||
trigger = "Validate the binary is site-neutral: every per-site identity knob (logo, origin, name) stays runtime-overridable in source so the shared image bakes only defaults. ADR-001 single-image deploy.",
|
||||
|
||||
preconditions = [
|
||||
"rg (ripgrep) available on PATH",
|
||||
"code/crates/server/src/theme.rs and shell/seo.rs exist",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "check-knobs-overridable",
|
||||
action = "Per-site identity must come from the consumer's runtime config, never baked/hardcoded. Assert: theme.rs reads the logo from the consumer's site.ncl [logo] (logo_from_site_config / site.ncl present); the home hero template does NOT hardcode a logo path (it uses the site_logo global); SITE_BASE_URL + SITE_NAME remain runtime env in seo.rs. A hardcoded logo or a baked identity is a neutrality violation.",
|
||||
cmd = "cd code && fails=0; have(){ if rg -q \"$1\" \"$2\"; then echo \"PASS $3\"; else echo \"FAIL $3\"; fails=$((fails+1)); fi; }; absent(){ if [ -z \"$(rg -n \"$1\" \"$2\" 2>/dev/null)\" ]; then echo \"PASS $3\"; else echo \"FAIL $3 (hardcoded)\"; fails=$((fails+1)); fi; }; have 'logo_from_site_config|site\\.ncl' crates/server/src/theme.rs 'logo from site.ncl'; absent 'src=\"/images/' crates/pages_htmx/templates/pages/home.j2 'home template no hardcoded image'; have '\\\"SITE_BASE_URL\\\"' crates/server/src/shell/seo.rs 'origin env'; have '\\\"SITE_NAME\\\"' crates/server/src/shell/seo.rs 'name env'; echo \"neutrality violations: $fails\"",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "report",
|
||||
action = "ALL PASS ⇒ the binary bakes only defaults; a consumer overrides logo/origin/name at deploy (single-image). ANY FAIL ⇒ a knob lost its env override and the binary would hardcode one site; restore the `std::env::var(...).unwrap_or(baked)` seam before the binary can claim neutrality.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "check-knobs-overridable" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
}
|
||||
111
.ontoref/reflection/modes/validate-purpose-alignment.ncl
Normal file
111
.ontoref/reflection/modes/validate-purpose-alignment.ncl
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
d.make_mode String {
|
||||
id = "validate-purpose-alignment",
|
||||
trigger = "Validate that purpose.ncl criteria are met before production publication: bilingual completeness, SEO criteria (hreflang, JSON-LD, meta descriptions, Core Web Vitals), audience relevance, and rendering/hydration stability. A single Hard failure blocks publication.",
|
||||
|
||||
preconditions = [
|
||||
".ontoref/ontology/purpose.ncl exists",
|
||||
".ontoref/ontology/state.ncl exists",
|
||||
".ontoref/ontology/gate.ncl exists",
|
||||
"nickel binary available on PATH",
|
||||
"code/site/config/ exists",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "export-purpose",
|
||||
action = "Export .ontoref/ontology/purpose.ncl via nickel. Validates the PurposeFile schema and all purpose-constraints checks. Fail immediately on any structural violation.",
|
||||
cmd = "nickel export .ontoref/ontology/purpose.ncl",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "check-rendering-state",
|
||||
action = "Export state.ncl and read the rendering-mode dimension. The Hard criterion 'no-publish-during-hydration-instability' requires that no hydration-opted route regresses below the htmx-ssr baseline. Run validate-hydration; any empty-HashMap hit in a *Client component is a Hard FAIL that blocks publication.",
|
||||
cmd = "nickel export .ontoref/ontology/state.ncl",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "export-purpose" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "check-purpose-gate-membrane",
|
||||
action = "Export gate.ncl and verify the 'purpose-gate' membrane exists, is active=true, and protocol='Challenge. A weakened or inactive purpose-gate is itself a Hard violation.",
|
||||
cmd = "nickel export .ontoref/ontology/gate.ncl",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "export-purpose" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "check-bilingual-completeness",
|
||||
action = "For each menu-visible content section (blog, projects): diff EN vs ES content file lists. Flag any section where ES trails EN. This is the Hard criterion 'bilingual-completeness'.",
|
||||
cmd = "diff <(find code/site/content/en -name '*.md' 2>/dev/null | sed 's#/en/#/#' | sort) <(find code/site/content/es -name '*.md' 2>/dev/null | sed 's#/es/#/#' | sort) | head -40 || echo 'content dirs not found at code/site/content/'",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "export-purpose" }],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "check-hreflang",
|
||||
action = "Audit generated HTML for the bilingual-canonical-urls criterion: verify <link rel=\"alternate\" hreflang=\"es\"> and hreflang=\"en\" are present in the <head> of blog and project pages, and that sitemap.xml includes both variants. Hard SEO criterion.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "export-purpose" }],
|
||||
note = "Requires a built or staged site. If none available, report UNVERIFIED (not PASS).",
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "check-structured-data",
|
||||
action = "Audit generated HTML for the structured-data-articles criterion: verify <script type=\"application/ld+json\"> with @type Article or TechArticle is present in blog post pages. Hard SEO criterion.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "export-purpose" }],
|
||||
note = "Requires a built or staged site. If none available, report UNVERIFIED.",
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "check-meta-descriptions",
|
||||
action = "Audit generated HTML for the meta-description-purpose-aligned criterion: verify every published page has a non-empty <meta name=\"description\"> matching the real content (no clickbait, no keyword stuffing). Spot-check 5 pages minimum. Hard SEO criterion.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "export-purpose" }],
|
||||
note = "Requires a built or staged site. Human judgment required for content accuracy.",
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "check-core-web-vitals",
|
||||
action = "Run Lighthouse (mobile) on /, /blog, /projects. Report the performance score; flag any score < 90. Soft SEO criterion 'core-web-vitals-threshold' (advisory, not blocking).",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "export-purpose" }],
|
||||
note = "Requires a running site: npx lighthouse <url> --only-categories=performance --form-factor=mobile --output=json",
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "validate-hard-criteria",
|
||||
action = "Aggregate results from check-rendering-state, check-bilingual-completeness, check-hreflang, check-structured-data, check-meta-descriptions. For each Hard criterion in purpose.ncl validation_criteria and seo_criteria, report PASS / FAIL / UNVERIFIED. A single Hard FAIL means BLOCKED.",
|
||||
actor = 'Both,
|
||||
depends_on = [
|
||||
{ step = "check-rendering-state" },
|
||||
{ step = "check-purpose-gate-membrane" },
|
||||
{ step = "check-bilingual-completeness" },
|
||||
{ step = "check-hreflang" },
|
||||
{ step = "check-structured-data" },
|
||||
{ step = "check-meta-descriptions" },
|
||||
{ step = "check-core-web-vitals" },
|
||||
],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
{
|
||||
id = "report",
|
||||
action = "Generate the purpose-alignment report: (1) per-criterion PASS/FAIL/UNVERIFIED table, Hard first; (2) Soft criteria status; (3) overall verdict READY FOR PUBLICATION or BLOCKED; (4) if BLOCKED, ordered list of blocking issues with the Hard criterion id and fix hint.",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "validate-hard-criteria" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
|
||||
postconditions = [
|
||||
"nickel export .ontoref/ontology/purpose.ncl succeeds",
|
||||
"rendering-mode hydration-opted routes confirmed at htmx-ssr baseline or better (validate-hydration clean)",
|
||||
"purpose-gate membrane is active with protocol='Challenge",
|
||||
"All Hard ValidationCriteria PASS or explicitly UNVERIFIED with justification",
|
||||
"All Hard SeoCriterion PASS or explicitly UNVERIFIED with justification",
|
||||
"Report generated with explicit READY or BLOCKED verdict and criterion table",
|
||||
],
|
||||
}
|
||||
37
.ontoref/reflection/modes/validate-route-superset.ncl
Normal file
37
.ontoref/reflection/modes/validate-route-superset.ncl
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
# Verifier for the route superset (ADR-001, single-image deploy).
|
||||
# The baked route table must be a SHARED SUPERSET: every standard content kind
|
||||
# {blog, projects, adr, catalog} must declare BOTH a ContentIndex (the /kind
|
||||
# listing) and a PostViewer (the /kind/{slug} detail) in site/config/routes.ncl,
|
||||
# so a consumer's menu/About link to that kind resolves against the shared binary
|
||||
# instead of 404-ing (the build/runtime split gotcha). Routing stays compiled
|
||||
# (ADR-001 forbids runtime-routing) — completeness is checked, not dynamism.
|
||||
# Runnable as `just validate-route-superset` (hard gate).
|
||||
|
||||
d.make_mode String {
|
||||
id = "validate-route-superset",
|
||||
trigger = "Validate the baked route table is a complete shared superset: ContentIndex + PostViewer present for every standard content kind so consumer links never 404. ADR-001 single-image deploy.",
|
||||
|
||||
preconditions = [
|
||||
"rg (ripgrep) available on PATH",
|
||||
"code/site/config/routes.ncl exists",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "check-superset-kinds",
|
||||
action = "For each standard content kind {blog, projects, adr, catalog}, assert site/config/routes.ncl declares both make_content_route \"ContentIndex\" \"<kind>\" and make_content_route \"PostViewer\" \"<kind>\". A missing index or viewer is a superset gap — a consumer linking to that kind 404s on the shared binary. FAIL names the kind and which record is missing.",
|
||||
cmd = "cd code && fails=0; for k in blog projects adr catalog; do i=$(rg -c \"make_content_route \\\"ContentIndex\\\" \\\"$k\\\"\" site/config/routes.ncl 2>/dev/null || true); p=$(rg -c \"make_content_route \\\"PostViewer\\\" \\\"$k\\\"\" site/config/routes.ncl 2>/dev/null || true); if [ \"${i:-0}\" -ge 1 ] && [ \"${p:-0}\" -ge 1 ]; then echo \"PASS $k\"; else echo \"FAIL $k (index=${i:-0} viewer=${p:-0})\"; fails=$((fails+1)); fi; done; echo \"superset gaps: $fails\"",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "report",
|
||||
action = "ALL kinds PASS ⇒ the baked route table is a complete superset and a consumer's links resolve. ANY FAIL ⇒ add the missing kind to site/config/routes.ncl (ContentIndex + PostViewer) and content.ncl (ContentType), then `just local-install` — the route table is baked, so a rebuild is required (ADR-001: routing stays compiled).",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "check-superset-kinds" }],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
}
|
||||
68
.ontoref/reflection/modes/validate-seam.ncl
Normal file
68
.ontoref/reflection/modes/validate-seam.ncl
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
let d = import "../defaults.ncl" in
|
||||
|
||||
# Verifier for the customization seam (core.ncl::customization-seam practice).
|
||||
# This mode IS the transition condition of the consumer-neutrality FSM dimension
|
||||
# (state.ncl): one-site-baked → neutral-framework requires these checks to pass.
|
||||
# Each check is a deterministic grep oracle that exits 0 and prints PASS/FAIL, so
|
||||
# the DAG runs to the report step regardless of individual verdicts.
|
||||
#
|
||||
# What it guards: brand, the site's own public origin, and site identity must NOT
|
||||
# be hardcoded in the shared Rust that the htmx-ssr binary actually compiles and
|
||||
# serves — crates/server/src + crates/pages_htmx/src. Those belong to the
|
||||
# runtime/config layers per the seam; a hit is the one-site-pole collapse the
|
||||
# framework-vs-one-site tension forbids.
|
||||
#
|
||||
# SCOPE — active surface only. crates/pages (Leptos hydration pages) is an
|
||||
# `optional` dep NOT pulled by the htmx-ssr feature (server Cargo.toml:
|
||||
# htmx-ssr = ["ssr","auth","email"], no dep:website-pages), so it is absent from
|
||||
# this binary; its external project-showcase links (vapora.dev, rustelo.dev, …)
|
||||
# are outbound CONTENT, not the site's own baked origin — checking them would
|
||||
# report false positives. If a route ever opts into leptos-hydration, widen the
|
||||
# scope to crates/pages/src and treat self-origin self-links (not outbound links)
|
||||
# as the violation.
|
||||
|
||||
d.make_mode String {
|
||||
id = "validate-seam",
|
||||
trigger = "Validate the customization seam: brand, public origin and site identity must live in the runtime/config layers, never hardcoded in shared Rust (crates/). Transition condition for the consumer-neutrality FSM dimension.",
|
||||
|
||||
preconditions = [
|
||||
"rg (ripgrep) available on PATH",
|
||||
"code/crates/server/src and code/crates/pages_htmx/src exist (the active htmx-ssr surface)",
|
||||
],
|
||||
|
||||
steps = [
|
||||
{
|
||||
id = "check-no-brand-hex",
|
||||
action = "No canonical brand hex (#E8A838 amber, #C0CCD8 silver, #0F1319 dark, #C88A20, #F0C265) may appear in the active shared Rust. Brand colour belongs in CSS (consumer styles + assets.ncl), never compiled in. Any FAIL line is a seam violation.",
|
||||
cmd = "m=$(rg -n -i '#(e8a838|c0ccd8|0f1319|c88a20|f0c265)' code/crates/server/src code/crates/pages_htmx/src 2>/dev/null); if [ -n \"$m\" ]; then echo 'FAIL brand-hex in shared code:'; echo \"$m\"; else echo 'PASS no brand hex in shared code'; fi",
|
||||
actor = 'Both,
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "check-no-hardcoded-origin",
|
||||
action = "No hardcoded public origin (a real site host such as ontoref.dev) may be baked as the site's own origin in the active shared Rust. The public origin comes from the SITE_BASE_URL env at runtime (seo::site_base_url) and self-links are relative. A localhost dev fallback is allowed; a baked production host is a violation. Scoped to server/src + pages_htmx/src so outbound content links in the optional Leptos pages are not false-flagged.",
|
||||
cmd = "m=$(rg -n 'https?://[a-z0-9.-]+\\.(dev|com|pro|org)' code/crates/server/src code/crates/pages_htmx/src -g '*.rs' 2>/dev/null | rg -v '//!|///' | rg -vi 'example\\.|w3\\.org|schema\\.org|localhost|nickel-lang|github\\.com|yourusername'); if [ -n \"$m\" ]; then echo 'FAIL hardcoded production origin in shared code:'; echo \"$m\"; else echo 'PASS no hardcoded production origin in shared code'; fi",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "check-no-brand-hex" }],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "check-no-site-identity",
|
||||
action = "No consumer-specific identity literal (logo filename, owner/site name like jesusperez or jpl-website) may appear in the active shared Rust. Identity comes from site/config (logo, site.name) — build-baked config, not compiled code.",
|
||||
cmd = "m=$(rg -n -i 'jesusperez|jpl-website' code/crates/server/src code/crates/pages_htmx/src -g '*.rs' 2>/dev/null | rg -v '//!|///'); if [ -n \"$m\" ]; then echo 'FAIL site identity literal in shared code:'; echo \"$m\"; else echo 'PASS no site identity literal in shared code'; fi",
|
||||
actor = 'Both,
|
||||
depends_on = [{ step = "check-no-brand-hex" }],
|
||||
on_error = { strategy = 'Continue },
|
||||
},
|
||||
{
|
||||
id = "report",
|
||||
action = "Aggregate the three checks. ALL PASS ⇒ the seam is respected and the consumer-neutrality transition is unblocked on this axis. ANY FAIL ⇒ a customization has leaked into shared Rust; report each offending file:line and the layer it belongs in (brand→CSS, origin→env, identity→site/config) so it can be moved before the binary can claim neutrality.",
|
||||
actor = 'Both,
|
||||
depends_on = [
|
||||
{ step = "check-no-hardcoded-origin" },
|
||||
{ step = "check-no-site-identity" },
|
||||
],
|
||||
on_error = { strategy = 'Stop },
|
||||
},
|
||||
],
|
||||
}
|
||||
134
.ontoref/reflection/schema.ncl
Normal file
134
.ontoref/reflection/schema.ncl
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
let _Dependency = {
|
||||
step | String,
|
||||
kind | [| 'Always, 'OnSuccess, 'OnFailure |] | default = 'Always,
|
||||
condition | String | optional,
|
||||
} in
|
||||
|
||||
let _OnError = {
|
||||
strategy | [| 'Stop, 'Continue, 'Retry, 'Fallback, 'Branch |],
|
||||
target | String | optional,
|
||||
on_success | String | optional,
|
||||
max | Number | default = 3,
|
||||
backoff_s | Number | default = 5,
|
||||
} in
|
||||
|
||||
# ── Guard ────────────────────────────────────────────────────────────────────
|
||||
# Executable pre-flight check that runs BEFORE any step in the mode.
|
||||
# If a guard fails, the mode prints the reason and aborts — preventing agents
|
||||
# and humans from executing procedures that violate active constraints.
|
||||
# Guards turn silent constraint violations into loud, early blocks.
|
||||
#
|
||||
# Pattern: Active Partner (#1 from Augmented Coding Patterns)
|
||||
# "Explicitly grant permission and encourage AI to push back."
|
||||
# Guards are the mechanism by which the protocol pushes back.
|
||||
#
|
||||
# cmd: shell command that exits 0 = pass, non-zero = blocked
|
||||
# reason: human-readable explanation shown when the guard blocks
|
||||
# severity: 'Block aborts execution, 'Warn prints warning but continues
|
||||
let _Guard = {
|
||||
id | String,
|
||||
cmd | String,
|
||||
reason | String,
|
||||
severity | [| 'Block, 'Warn |] | default = 'Block,
|
||||
} in
|
||||
|
||||
# ── Converge ─────────────────────────────────────────────────────────────────
|
||||
# Post-execution convergence check for iterative modes.
|
||||
# After all steps complete, the executor evaluates the condition command.
|
||||
# If it exits non-zero, the mode re-executes (failed steps or all steps)
|
||||
# up to max_iterations times.
|
||||
#
|
||||
# Pattern: Refinement Loop (#36 from Augmented Coding Patterns)
|
||||
# "Each iteration removes a layer of noise, making the next layer visible."
|
||||
#
|
||||
# condition: shell command — exit 0 = converged, non-zero = iterate again
|
||||
# max_iterations: upper bound on re-execution cycles (prevents infinite loops)
|
||||
# strategy: 'RetryFailed re-runs only steps that failed or were blocked;
|
||||
# 'RetryAll re-runs the entire DAG from scratch
|
||||
let _Converge = {
|
||||
condition | String,
|
||||
max_iterations | Number | default = 3,
|
||||
strategy | [| 'RetryFailed, 'RetryAll |] | default = 'RetryFailed,
|
||||
} in
|
||||
|
||||
let _ActionStep = fun ActionContract => {
|
||||
id | String,
|
||||
action | ActionContract,
|
||||
depends_on | Array _Dependency | default = [],
|
||||
cmd | String | optional,
|
||||
actor | [| 'Human, 'Agent, 'Both |] | default = 'Both,
|
||||
on_error | _OnError | default = { strategy = 'Stop },
|
||||
verify | String | optional,
|
||||
note | String | optional,
|
||||
} in
|
||||
|
||||
# Resolution strategy for multi-level hierarchy traversal (ADR-018).
|
||||
# Required at Domain (level 2) and Instance (level 3) — absent at Base (level 1).
|
||||
# Implicit absence at level 2+ is treated as 'Delegate with a Soft validation warning.
|
||||
let _ResolutionStrategy = [| 'Override, 'Delegate, 'Merge, 'Compose |] in
|
||||
|
||||
let _ModeBase = fun ActionContract => {
|
||||
id | String,
|
||||
trigger | String,
|
||||
preconditions | Array String | default = [],
|
||||
guards | Array _Guard | default = [],
|
||||
steps | Array (_ActionStep ActionContract),
|
||||
postconditions | Array String | default = [],
|
||||
converge | _Converge | optional,
|
||||
# ADR-018: which level owns this mode's implementation.
|
||||
# 'Override = complete here; 'Delegate = traverse to parent; 'Merge = accumulate;
|
||||
# 'Compose = partial inheritance declared in `extends`.
|
||||
strategy | _ResolutionStrategy | optional,
|
||||
# When strategy = 'Compose: step or field ids to inherit from the parent mode.
|
||||
# Every reference must exist in the parent — a broken extends is always a bug.
|
||||
extends | Array String | optional,
|
||||
} in
|
||||
|
||||
# DAG-validated Mode contract:
|
||||
# 1. structural contract via _ModeBase
|
||||
# 2. step ID uniqueness within the mode
|
||||
# 3. referential integrity — all depends_on.step reference an existing id
|
||||
# Cycle detection is a separate Rust-side pass (ontoref-reflection::dag::validate).
|
||||
let _Mode = fun ActionContract =>
|
||||
std.contract.custom (fun label value =>
|
||||
let validated = value | (_ModeBase ActionContract) in
|
||||
let steps = validated.steps in
|
||||
let ids = steps |> std.array.map (fun s => s.id) in
|
||||
|
||||
let _after_unique = ids |> std.array.fold_left (fun acc id =>
|
||||
if std.record.has_field id acc.seen then
|
||||
std.contract.blame_with_message
|
||||
"Mode '%{validated.id}': duplicate step id '%{id}'"
|
||||
label
|
||||
else
|
||||
{ seen = acc.seen & { "%{id}" = true }, ok = true }
|
||||
) { seen = {}, ok = true } in
|
||||
|
||||
let bad_refs = steps |> std.array.flat_map (fun step =>
|
||||
step.depends_on
|
||||
|> std.array.filter (fun dep =>
|
||||
!(ids |> std.array.any (fun i => i == dep.step))
|
||||
)
|
||||
|> std.array.map (fun dep =>
|
||||
"step '%{step.id}' depends_on unknown '%{dep.step}'"
|
||||
)
|
||||
) in
|
||||
|
||||
if std.array.length bad_refs > 0 then
|
||||
std.contract.blame_with_message
|
||||
"Mode '%{validated.id}' has invalid depends_on: %{std.string.join ", " bad_refs}"
|
||||
label
|
||||
else
|
||||
'Ok validated
|
||||
)
|
||||
in
|
||||
|
||||
{
|
||||
Dependency = _Dependency,
|
||||
OnError = _OnError,
|
||||
Guard = _Guard,
|
||||
Converge = _Converge,
|
||||
ActionStep = _ActionStep,
|
||||
Mode = _Mode,
|
||||
ResolutionStrategy = _ResolutionStrategy,
|
||||
}
|
||||
33
.ontoref/reflection/schemas/backlog.ncl
Normal file
33
.ontoref/reflection/schemas/backlog.ncl
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
let status_type = [| 'Open, 'InProgress, 'Done, 'Cancelled |] in
|
||||
let priority_type = [| 'Critical, 'High, 'Medium, 'Low |] in
|
||||
let kind_type = [| 'Todo, 'Wish, 'Idea, 'Bug, 'Debt |] in
|
||||
let graduate_type = [| 'Adr, 'Mode, 'StateTransition, 'PrItem |] in
|
||||
|
||||
let item_type = {
|
||||
id | String,
|
||||
title | String,
|
||||
kind | kind_type,
|
||||
priority | priority_type | default = 'Medium,
|
||||
status | status_type | default = 'Open,
|
||||
detail | String | default = "",
|
||||
# Optional links to existing artifacts
|
||||
related_adrs | Array String | default = [],
|
||||
related_modes | Array String | default = [],
|
||||
related_dim | String | optional, # state.ncl dimension id
|
||||
# Graduation target — when this item is ready to be promoted
|
||||
graduates_to | graduate_type | optional,
|
||||
# ISO date strings
|
||||
created | String | default = "",
|
||||
updated | String | default = "",
|
||||
} in
|
||||
|
||||
{
|
||||
Status = status_type,
|
||||
Priority = priority_type,
|
||||
Kind = kind_type,
|
||||
GraduateTo = graduate_type,
|
||||
Item = item_type,
|
||||
BacklogConfig = {
|
||||
items | Array item_type,
|
||||
},
|
||||
}
|
||||
29
.rustelo.ontoref/README.md
Normal file
29
.rustelo.ontoref/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# `.rustelo.ontoref/` — Rustelo domain implementation declaration
|
||||
|
||||
This directory declares that **website-htmx-rustelo is an implementation of the
|
||||
Rustelo framework**. Its presence (a `.<domain>.ontoref/` marker at the project
|
||||
root) is how `ontoref` detects this project as a consumer of the `rustelo`
|
||||
domain; the `repo_kind = 'RusteloApp` in `.ontoref/ontology/manifest.ncl`
|
||||
activates the domain extension (`ontoref rustelo layers|plugins|routes|wasm`).
|
||||
|
||||
## Contents
|
||||
|
||||
- `app.ncl` — the concrete app-surface declaration (layers, routes, rendering
|
||||
profiles, WASM targets) validated against the rustelo domain contracts. The
|
||||
contract schema is authored in the rustelo project and resolved by ontoref via
|
||||
the domain's `schema_cmd` (`ontoref project root rustelo`).
|
||||
Runnable reflection modes for this implementation live in the spine, where the
|
||||
ontoref mode runner discovers them: `.ontoref/reflection/modes/` —
|
||||
`validate-content` (bilingual EN/ES parity), `validate-hydration` (SSR/WASM
|
||||
parity for hydration-opted routes), `sync-ontology` (ontology↔artifact drift),
|
||||
`validate-purpose-alignment` (publication-readiness against `purpose.ncl` Hard
|
||||
criteria), and `plan-publication` (dependency-ordered content/SEO publication
|
||||
plan). Run them with `ontoref run <mode>` from the project root. Publication
|
||||
intent and criteria are declared in `.ontoref/ontology/purpose.ncl`, gated by
|
||||
the `purpose-gate` membrane in `gate.ncl`.
|
||||
|
||||
## Authority
|
||||
|
||||
The domain layer (contracts, commands) is authored in the **rustelo** project
|
||||
and the ontoref `domains/rustelo/` extension — not here. This marker references
|
||||
that authority for cross-project browsing.
|
||||
47
.rustelo.ontoref/app.ncl
Normal file
47
.rustelo.ontoref/app.ncl
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# .rustelo.ontoref/app.ncl — Rustelo implementation declaration.
|
||||
#
|
||||
# Declares this project's app surface against the rustelo domain contracts.
|
||||
# The schema is authored in the rustelo project and resolved by ontoref via the
|
||||
# domain's schema_cmd (`ontoref project root rustelo`). The absolute import below
|
||||
# is that resolved root; the runtime substitutes %{schema_root} when browsing.
|
||||
let r = import "/Users/Akasha/Development/rustelo/domains/rustelo/ontology/schema.ncl" in
|
||||
|
||||
r.make_app {
|
||||
app = "website-htmx-rustelo",
|
||||
default_profile = 'Htmx,
|
||||
description = "Bilingual EN/ES website served through Rustelo's HTMX rendering profile with Minijinja templates.",
|
||||
|
||||
layers = [
|
||||
r.make_layer {
|
||||
id = "pages-htmx",
|
||||
paths = ["crates/pages_htmx/"],
|
||||
profile = 'Htmx,
|
||||
description = "HTMX/Minijinja page and fragment renderers — the default rendering surface.",
|
||||
},
|
||||
r.make_layer {
|
||||
id = "server",
|
||||
paths = ["crates/server/"],
|
||||
profile = 'Htmx,
|
||||
description = "Axum server: routing, SSR, htmx fragment endpoints, content APIs.",
|
||||
},
|
||||
r.make_layer {
|
||||
id = "client",
|
||||
paths = ["crates/client/"],
|
||||
profile = 'Hydrate,
|
||||
description = "Leptos WASM client for routes that opt into hydration (dual-mode capability).",
|
||||
},
|
||||
],
|
||||
|
||||
routes = [
|
||||
r.make_route { path = "/", handler = "home", profile = 'Htmx, languages = ["en", "es"] },
|
||||
],
|
||||
|
||||
wasm_targets = [
|
||||
r.make_wasm {
|
||||
name = "website",
|
||||
output = "target/site/pkg",
|
||||
features = ["hydrate"],
|
||||
hydrate = true,
|
||||
},
|
||||
],
|
||||
}
|
||||
0
README.md
Normal file
0
README.md
Normal file
Loading…
Add table
Reference in a new issue