First real commit of site/. Until now outreach/.git tracked exactly one file
(README.md) and every content pack stamped git_sha=5619a40 regardless of what it
shipped — a signature with no provenance behind it. The site had no witness, which
is why a deploy could erase two sessions of work with nothing to diff against and
nothing to restore from.
Tracked here (authored): site/content, site/config, site/i18n, site/public/images,
justfile, scripts/, .just/, run*.sh, rustelo.manifest.toml.
Ignored (reproducible, or secret):
.k, .env real keys — outreach mirrors to a PUBLIC remote
site/r, site/public/r content indexes, rebuilt by `just content`
provisioning/.content-packs 228 MB of deploy tarballs
cache, data, logs_*.out runtime droppings
Three trees are deliberately NOT ignored despite looking generated, because their
declared source cannot currently reproduce them — ignoring them would delete the
only copy:
site/public/images 196 of 209 images exist nowhere else (assets/site is empty)
site/content/{adr,catalog} projections whose generators are unwired (gen-adr-pages)
site/content/domains or failing (`just graph`)
They become ignorable when an audit proves regeneration is a no-op, not before.
Fixes carried in this import
---------------------------
content: the mirror ran one way and reverted its own work. content_processor writes
site/r, but the recipe ended with `rsync -a site/public/r/ -> site/r/`, so each fresh
index was clobbered by the stale deploy-tree copy: the tool reported "index.json with
69 posts" while the disk kept 58. Meanwhile public/r is the tree the deploy pack ships,
so no new content could ever reach production. The two trees own different files, so
the mirror now runs both ways: content indexes r -> public/r (with --delete, or a
renamed slug survives as a live URL), and the four nu artifacts (about, adr-map,
taglines, content_graph) back public/r -> r, excluded from the outbound leg so stale
copies cannot overwrite the fresh originals.
templates: the assembler has two layers (framework defaults, source-project templates)
but the level chain has three (rustelo -> website-htmx-rustelo -> outreach/site). The
site level had no overlay slot, so its template work had nowhere legitimate to live and
ended up in htmx-templates/ — the one tree `just templates` rm -rf's. That is how the
nav submenus were lost. site/templates-overlay/ is the missing third slot; 214 lines
(nav submenus, subscribe CTA, post engagement block) now live in a declared source and
survive regeneration byte-identical.
adr: projected ADR-059..069 into the content tree. They were written months ago and
never reached the site because gen-adr-pages.nu is wired into no recipe — a generator
outside the dependency chain is a generator that does not run.
Gates added
-----------
templates-check reassembles into a temp tree and asserts an empty diff against the
live one: any drift means htmx-templates/ holds work that exists
live one: any drift means htmx-templates/ holds work that exists
nowhere else and the next run deletes it.
Both gates run before the destructive operation, not after. A check you can only run
afterwards is not a gate, it is an autopsy.
Refs: ADR-062 (projection-not-own-repo: site is a Projection of outreach, not its own
repo — different deploy target is not a different visibility boundary), ADR-048
(materialization), ADR-066 (the check decides, never the reporter — which the content
pipeline, the surface that publishes ontoref, was the last place not to honour).
382 lines
21 KiB
Text
382 lines
21 KiB
Text
#!/usr/bin/env nu
|
|
|
|
# Project a level's About surface from its about.ncl contract + its .ontoref/.
|
|
#
|
|
# A single view-model is assembled daemon-free and shaped as a GENERIC BLOCK
|
|
# vocabulary so new sections are projector-only changes (no template/handler edit
|
|
# in the site framework). A section = { key, title, blocks }; each block is a
|
|
# typed render primitive the consumers know how to render:
|
|
#
|
|
# prose { type, html } raw HTML body
|
|
# timeline { type, started?, items:[{date, text{en,es}}] }
|
|
# cards { type, groups:[{label, items:[{title, meta?, body?}]}] }
|
|
# links { type, groups:[{label?, items:[{href, code?, text}]}], browse?{href,label{en,es}} }
|
|
# graph { type, svg?, live?{href, label{en,es}} } raw mini-SVG + daemon CTA
|
|
# kv { type, rows:[{k, v}] }
|
|
#
|
|
# Two consumers of the view-model, both rendering the SAME block vocabulary:
|
|
# static (Option A) : self-contained brand-wrapped HTML → outreach/web/about/<level>/
|
|
# --emit-json (B) : about.json for the SSR /about handler (hot-reloaded)
|
|
#
|
|
# Adding a section of an existing block type → edit this projector only. A brand
|
|
# new block type → also extend about_project.j2 (the render-primitive vocabulary,
|
|
# in the site framework) and vm-block-html below. That is the only coupling left.
|
|
#
|
|
# kind = 'Personal levels are skipped: their About stays about.j2.
|
|
#
|
|
# Usage (from outreach/site):
|
|
# nu scripts/build/gen-about-pages.nu --root ../.. --out ../../outreach/web/about --daemon https://ontoref.dev
|
|
# nu scripts/build/gen-about-pages.nu --root ../.. --emit-json site/public/r/about.json --daemon https://ontoref.dev
|
|
|
|
use lib/doc-page.nu [esc paras sec page-shell]
|
|
|
|
const TITLES = {
|
|
philosophy: { en: "Philosophy", es: "Filosofía" },
|
|
features: { en: "Features", es: "Características" },
|
|
why: { en: "Why it matters", es: "Por qué importa" },
|
|
history: { en: "History", es: "Historia" },
|
|
defs: { en: "What it is", es: "Qué es" },
|
|
graph: { en: "Graph", es: "Grafo" },
|
|
adrs: { en: "Architecture Decisions", es: "Decisiones de Arquitectura" },
|
|
catalog: { en: "Catalog of verifiables", es: "Catálogo de verificables" },
|
|
glossary: { en: "Glossary", es: "Glosario" },
|
|
}
|
|
|
|
def hook [text: string, cap: int] {
|
|
let first = ($text | str trim | split row "\n\n" | first | default "" | str trim)
|
|
if (($first | str length) <= $cap) { $first } else { (($first | str substring 0..$cap) | str trim) + "…" }
|
|
}
|
|
|
|
def select-defs [nodes: list, level: string, mode: string, ids: list] {
|
|
let pool = ($nodes | where {|n| ($n.level? | default "") == $level })
|
|
if ($mode == "All") { $pool
|
|
} else if ($mode == "Selected") { ($pool | where {|n| ($n.id? | default "") in $ids })
|
|
} else { [] }
|
|
}
|
|
|
|
# A block carries renderable content for its type. Empty blocks are dropped so a
|
|
# section with no content (sparse .ontoref/) disappears rather than rendering hollow.
|
|
def block-nonempty [b: record] {
|
|
let t = $b.type
|
|
if $t == "prose" { ($b.html? | default "" | str trim | is-not-empty)
|
|
} else if $t == "timeline" { (($b.items? | default [] | is-not-empty) or (($b.started? | default "") | is-not-empty))
|
|
} else if $t == "cards" { (($b.groups? | default []) | any {|g| ($g.items? | default []) | is-not-empty })
|
|
} else if $t == "links" { (($b.groups? | default []) | any {|g| ($g.items? | default []) | is-not-empty })
|
|
} else if $t == "graph" { (($b.svg? | default "" | is-not-empty) or (($b.live?.href? | default "") | is-not-empty))
|
|
} else if $t == "kv" { ($b.rows? | default [] | is-not-empty)
|
|
} else { false }
|
|
}
|
|
|
|
# ── history from CHANGELOG (sourced, dated from the ADRs each release accepts) ─
|
|
|
|
# Latest `date = "YYYY-MM-DD"` declared in adr-NNN-*.ncl, or "" if none.
|
|
def adr-date [root: string, num: string] {
|
|
let padded = ($num | into int | fill --alignment right --character '0' --width 3)
|
|
let files = (glob $"($root)/.ontoref/adrs/adr-($padded)-*.ncl")
|
|
if ($files | is-empty) { "" } else {
|
|
let m = (open --raw ($files | first) | parse --regex 'date\s*=\s*"(?<d>\d{4}-\d{2}-\d{2})"')
|
|
if ($m | is-empty) { "" } else { ($m | get d | sort | last) }
|
|
}
|
|
}
|
|
|
|
# One timeline milestone per `## [version]` CHANGELOG release, in file order
|
|
# (newest first). The row date is the latest ADR date the release references
|
|
# (falls back to the version tag); the text is the bilingual `releases` label,
|
|
# falling back to the release's first `### ` headline.
|
|
def changelog-milestones [root: string, releases: list] {
|
|
let path = $"($root)/code/CHANGELOG.md"
|
|
if not ($path | path exists) { return [] }
|
|
let lines = (open --raw $path | lines)
|
|
let hdr_idx = ($lines | enumerate | where {|r| ($r.item | str starts-with "## [") } | get index)
|
|
if ($hdr_idx | is-empty) { return [] }
|
|
let n = ($hdr_idx | length)
|
|
$hdr_idx | enumerate | each {|e|
|
|
let start = $e.item
|
|
let end = (if ($e.index + 1 < $n) { ($hdr_idx | get ($e.index + 1)) } else { ($lines | length) })
|
|
let block = ($lines | slice $start..($end - 1))
|
|
let version = ($block | first | parse --regex '## \[(?<v>[^\]]+)\]' | get v.0? | default "")
|
|
let heads = ($block | skip 1 | where {|l| ($l | str starts-with "### ") })
|
|
let headline = ($heads | get 0? | default "" | str replace --regex '^###\s+' '')
|
|
let body = ($block | str join "\n")
|
|
# Release cut date = the latest of: the `Session(s) YYYY-MM-DD` token the entry
|
|
# carries, and the dates of the ADRs this release INTRODUCES (its ### headings
|
|
# + **ADR-NNN** bold refs) — never every ADR cross-referenced in prose.
|
|
let head_nums = ($heads | str join "\n" | parse --regex '(?i)adr-(?<x>\d+)' | get x)
|
|
let bold_nums = ($body | parse --regex '\*\*ADR-(?<x>\d+)\*\*' | get x)
|
|
let nums = ($head_nums | append $bold_nums | uniq)
|
|
let adr_dates = ($nums | each {|x| adr-date $root $x } | where {|d| ($d | is-not-empty) })
|
|
let sess_dates = ($body | parse --regex '(?i)sessions?[^\n]*?(?<d>\d{4}-\d{2}-\d{2})' | get d)
|
|
let dates = ($adr_dates | append $sess_dates | where {|d| ($d | is-not-empty) })
|
|
let date = (if ($dates | is-empty) { $version } else { ($dates | sort | last | str substring 0..9) })
|
|
let lbl = ($releases | where {|r| ($r.version? | default "") == $version } | get 0.line? | default null)
|
|
let text = (if ($lbl != null) { $lbl } else { { en: $headline, es: $headline } })
|
|
{ date: $date, text: $text }
|
|
}
|
|
}
|
|
|
|
# ── view-model assembly → sections of typed blocks ────────────────────────────
|
|
|
|
def build-vm [about: record, root: string, ip: string, daemon: string, nodes: list, adr_map: record, mini_graphs: record] {
|
|
let p = $about.project
|
|
|
|
# philosophy → prose (lead, bilingual) + kv (built with). Projected from card.ncl.
|
|
let phil = ($p.philosophy? | default null)
|
|
let philosophy_blocks = if ($phil == null) { [] } else {
|
|
let lead = ($phil.lead? | default { en: "", es: "" })
|
|
let bw = ($phil.built_with? | default [])
|
|
([
|
|
{ type: "prose", html: $"<p>(esc ($lead.en? | default ''))</p>", html_es: $"<p>(esc ($lead.es? | default ''))</p>" }
|
|
(if ($bw | is-empty) { null } else {
|
|
{ type: "kv", rows: [ { k: "Built with", k_es: "Construido con", v: ($bw | str join " · ") } ] }
|
|
})
|
|
] | where {|b| $b != null })
|
|
}
|
|
|
|
# features → cards (title-only, bilingual). card.ncl.features, translated.
|
|
let feat = ($p.features? | default null)
|
|
let features_blocks = if ($feat == null) { [] } else {
|
|
let items = ($feat.items? | default [])
|
|
[ { type: "cards", groups: [ { label: "Key ideas", items: ($items | each {|it| { title: ($it.en? | default ""), title_es: ($it.es? | default "") } }) } ] } ]
|
|
}
|
|
|
|
# why → prose (lead) + cards (titled differentiator points). Projected from positioning.
|
|
let why = ($p.why? | default null)
|
|
let why_blocks = if ($why == null) { [] } else {
|
|
let lead = ($why.lead? | default null)
|
|
let pts = ($why.points? | default [])
|
|
([
|
|
(if ($lead == null) { null } else { { type: "prose", html: $"<p>(esc ($lead.en? | default ''))</p>", html_es: $"<p>(esc ($lead.es? | default ''))</p>" } })
|
|
(if ($pts | is-empty) { null } else {
|
|
{ type: "cards", groups: [ { label: "", items: ($pts | each {|pt| {
|
|
title: ($pt.title.en? | default ""), title_es: ($pt.title.es? | default ""),
|
|
body: ($pt.body.en? | default ""), body_es: ($pt.body.es? | default ""),
|
|
} }) } ] }
|
|
})
|
|
] | where {|b| $b != null })
|
|
}
|
|
|
|
# history → prose (narrative, if authored) + timeline. source='Changelog derives
|
|
# dated rows from code/CHANGELOG.md (sourced); 'Manual keeps hand-listed milestones.
|
|
let h = ($p.history? | default {})
|
|
let narr_path = $"($root)/.ontoref/($h.narrative_path? | default '')"
|
|
let narrative = if (($h.narrative_path? | default "" | is-not-empty) and ($narr_path | path exists)) { (open --raw $narr_path) } else { "" }
|
|
# changelog-derived releases (recent) merged with any hand-declared `milestones`
|
|
# (the genesis/foundational rows the CHANGELOG version tags don't reach), newest
|
|
# first. So source='Changelog still shows where the project STARTS.
|
|
let cl_items = if (($h.source? | default "Manual") == "Changelog") {
|
|
(changelog-milestones $root ($h.releases? | default []))
|
|
} else { [] }
|
|
let manual_items = (($h.milestones? | default []) | each {|m| { date: ($m.date? | default ""), text: ($m.line? | default { en: "", es: "" }) } })
|
|
let tl_items = ($cl_items | append $manual_items | sort-by date --reverse)
|
|
let history_blocks = [
|
|
{ type: "prose", html: $narrative }
|
|
{ type: "timeline", started: ($h.started_at? | default ""), items: $tl_items }
|
|
]
|
|
|
|
# defs → cards grouped by level
|
|
let dsel = ($p.defs? | default {})
|
|
let def_specs = [
|
|
[label level mode ids];
|
|
["Axioms" "Axiom" ($dsel.axioms_mode? | default "All") ($dsel.axioms_ids? | default [])]
|
|
["Tensions" "Tension" ($dsel.tensions_mode? | default "All") ($dsel.tensions_ids? | default [])]
|
|
["Practices" "Practice" ($dsel.practices_mode? | default "Selected") ($dsel.practices_ids? | default [])]
|
|
]
|
|
let def_groups = ($def_specs | each {|g|
|
|
let sel = (select-defs $nodes $g.level $g.mode $g.ids)
|
|
{ label: $g.label, items: ($sel | each {|n| { title: ($n.name? | default ($n.id? | default "")), meta: ($n.pole? | default ""), body: (hook ($n.description? | default "") 240) } }) }
|
|
} | where {|grp| ($grp.items | is-not-empty) })
|
|
|
|
# glossary → cards grouped by category, bilingual (term.name/.definition .en/.es).
|
|
# The es fields carry the anglicism-free Spanish terms; the SSR /about handler
|
|
# renders them on the ES surface. Projected from .ontoref/ontology/glossary.ncl.
|
|
let gloss_export = (do { ^nickel export --format json --import-path $ip --import-path $"($root)/.ontoref" $"($root)/.ontoref/ontology/glossary.ncl" } | complete)
|
|
if $gloss_export.exit_code != 0 { error make { msg: $"glossary export failed: ($gloss_export.stderr)" } }
|
|
let gloss_terms = ($gloss_export.stdout | from json | get terms)
|
|
let gloss_cats = [
|
|
[key label];
|
|
["Discipline" "Disciplines"]
|
|
["Concept" "Concepts"]
|
|
["Practice" "Practices"]
|
|
["Artifact" "Artifacts"]
|
|
["Procedure" "Procedures"]
|
|
["Antipattern" "Antipatterns"]
|
|
]
|
|
let glossary_groups = ($gloss_cats | each {|c|
|
|
let items = ($gloss_terms | where category == $c.key | each {|t| {
|
|
title: ($t.name.en? | default ($t.id? | default "")),
|
|
title_es: ($t.name.es? | default ($t.name.en? | default "")),
|
|
meta: (($t.aliases? | default []) | append [""] | first),
|
|
body: (hook ($t.definition.en? | default "") 240),
|
|
body_es: (hook ($t.definition.es? | default "") 240),
|
|
} })
|
|
{ label: $c.label, items: $items }
|
|
} | where {|g| ($g.items | is-not-empty) })
|
|
|
|
# graph → the generated diagram image (preferred), else a content_graph mini-svg,
|
|
# plus the daemon live CTA. The image is a standalone file served at /r/.
|
|
let g = ($p.graph? | default {})
|
|
let focus = ($g.focus? | default "")
|
|
let diagram_file = $"($root)/outreach/site/site/public/images/ontoref-diagram.svg"
|
|
let mini = (if ($diagram_file | path exists) {
|
|
"<img src=\"/images/ontoref-diagram.svg\" alt=\"ontoref architecture diagram\" class=\"about-diagram\" style=\"width:100%;height:auto\" loading=\"lazy\"/>"
|
|
} else if ($focus | is-not-empty) and ($focus in $mini_graphs) { ($mini_graphs | get $focus) } else { "" })
|
|
# live link → the self-contained navigable Cytoscape page (daemon-free, same for
|
|
# both languages — node names are the labels). The static diagram is the hook.
|
|
let live = ($g.live_view? | default "/images/ontoref-graph.html")
|
|
let live_href = (if ($live | is-empty) { "" } else { $"($daemon)($live)" })
|
|
let graph_block = { type: "graph", svg: $mini, live: { href: $live_href, label: { en: "Open the interactive graph", es: "Abrir el grafo interactivo" } } }
|
|
|
|
# adrs → links (one group) + browse
|
|
let ainc = ($p.adrs? | default {})
|
|
let abase = ($ainc.route_base? | default "/adr")
|
|
let astatuses = ($ainc.statuses? | default [])
|
|
let aentries = ($adr_map | transpose id v | each {|e| { num: ($e.id | str replace 'adr-' ''), name: ($e.v.name? | default ""), status: ($e.v.status? | default "") } } | sort-by num)
|
|
let afiltered = (if ($astatuses | is-empty) { $aentries } else { $aentries | where {|e| $e.status in $astatuses } })
|
|
let alimit = ($ainc.digest_limit? | default 0)
|
|
let aitems = (if ($alimit > 0) { $afiltered | first $alimit } else { $afiltered })
|
|
let adrs_browse = (if ($ainc.show_browse? | default true) { { browse: { href: $abase, label: { en: "Browse all ADRs", es: "Ver todas las ADR" } } } } else { {} })
|
|
let adrs_block = ({
|
|
type: "links",
|
|
groups: [ { label: "", items: ($aitems | each {|e| { href: $"($abase)/($e.num)", code: $"ADR-($e.num)", text: $e.name } }) } ],
|
|
} | merge $adrs_browse)
|
|
|
|
# catalog → links grouped by kind + browse
|
|
let cinc = ($p.catalog? | default {})
|
|
let cbase = ($cinc.route_base? | default "/catalog")
|
|
let ckinds = ($cinc.kinds? | default ["operations" "validators"])
|
|
let cgroups = ($ckinds | each {|kind|
|
|
let dir = $"($root)/.ontoref/catalog/($kind)"
|
|
if not ($dir | path exists) { null } else {
|
|
let files = (glob $"($dir)/*.ncl" | where {|f| ($f | path basename) not-in ["schema.ncl" "_template.ncl"] } | sort)
|
|
let items = ($files | each {|f|
|
|
let rec = (try { nickel export --format json --import-path $ip --import-path $"($root)/.ontoref/catalog" $f | from json } catch { null })
|
|
if ($rec == null) { null } else { { href: $"($cbase)/($rec.id)", code: (if ($kind == "operations") { "op" } else { "val" }), text: $rec.id } }
|
|
} | where {|x| $x != null })
|
|
{ label: ($kind | str title-case), items: $items }
|
|
}
|
|
} | where {|x| ($x != null) and ($x.items | is-not-empty) })
|
|
let catalog_browse = (if ($cinc.show_browse? | default true) { { browse: { href: $cbase, label: { en: "Browse the full catalog", es: "Ver el catálogo completo" } } } } else { {} })
|
|
let catalog_block = ({ type: "links", groups: $cgroups } | merge $catalog_browse)
|
|
|
|
let blocks_for = {
|
|
philosophy: $philosophy_blocks,
|
|
features: $features_blocks,
|
|
why: $why_blocks,
|
|
history: $history_blocks,
|
|
defs: [ { type: "cards", groups: $def_groups } ],
|
|
glossary: [ { type: "cards", groups: $glossary_groups } ],
|
|
graph: [ $graph_block ],
|
|
adrs: [ $adrs_block ],
|
|
catalog: [ $catalog_block ],
|
|
}
|
|
|
|
let order = ($p.sections_order? | default ["philosophy" "features" "why" "history" "defs" "glossary" "graph" "adrs" "catalog"])
|
|
let sections = ($order | each {|k|
|
|
let blocks = (($blocks_for | get $k) | where {|b| block-nonempty $b })
|
|
if ($blocks | is-empty) { null } else { { key: $k, title: ($TITLES | get $k), blocks: $blocks } }
|
|
} | where {|s| $s != null })
|
|
|
|
{
|
|
kind: ($about.kind | str downcase),
|
|
level_id: $about.level_id,
|
|
hero: ($about.hero? | default { en: "", es: "" }),
|
|
sections: $sections,
|
|
}
|
|
}
|
|
|
|
# ── Option A: static HTML from the block vocabulary (English primary) ─────────
|
|
|
|
def vm-block-html [b: record] {
|
|
let t = $b.type
|
|
if $t == "prose" {
|
|
(paras $b.html)
|
|
} else if $t == "timeline" {
|
|
let started = (if ($b.started | is-empty) { "" } else { $"<p class=\"doc-meta\" style=\"display:block\">Started (esc $b.started)</p>" })
|
|
let rows = ($b.items | each {|m| $"<li><span class=\"doc-code\">(esc $m.date)</span><div><p>(esc ($m.text.en? | default ''))</p></div></li>" } | str join)
|
|
$"($started)<ul class=\"doc-list\">($rows)</ul>"
|
|
} else if $t == "cards" {
|
|
($b.groups | each {|grp|
|
|
let cards = ($grp.items | each {|n|
|
|
let meta = (if (($n.meta? | default "") | is-not-empty) { $"<span class=\"doc-badge doc-sev-soft\">(esc $n.meta)</span>" } else { "" })
|
|
$"<li><div><p class=\"doc-alt-opt\">(esc $n.title)($meta)</p><p class=\"doc-alt-why\">(esc ($n.body? | default ''))</p></div></li>"
|
|
} | str join)
|
|
$"<h3 style=\"color:var\(--onto-amber\);font-size:.95rem;margin:1rem 0 .25rem\">(esc $grp.label) \(($grp.items | length)\)</h3><ul class=\"doc-list\">($cards)</ul>"
|
|
} | str join)
|
|
} else if $t == "links" {
|
|
let groups = ($b.groups | each {|grp|
|
|
let head = (if (($grp.label? | default "") | is-not-empty) { $"<h3 style=\"color:var\(--onto-amber\);font-size:.95rem;margin:1rem 0 .25rem\">(esc $grp.label) \(($grp.items | length)\)</h3>" } else { "" })
|
|
let rows = ($grp.items | each {|i|
|
|
let code = (if (($i.code? | default "") | is-not-empty) { $"<code>(esc $i.code)</code> " } else { "" })
|
|
$"<li><a href=\"($i.href)\">($code)(esc $i.text)</a></li>"
|
|
} | str join)
|
|
$"($head)<ul class=\"doc-idx-list\">($rows)</ul>"
|
|
} | str join)
|
|
let browse = (if (($b.browse?.href? | default "") | is-not-empty) { $"<p><a class=\"doc-nav-btn\" href=\"($b.browse.href)\">(esc ($b.browse.label.en? | default 'Browse'))</a></p>" } else { "" })
|
|
$"($groups)($browse)"
|
|
} else if $t == "graph" {
|
|
let mini_html = (if (($b.svg? | default "") | is-empty) { "" } else { $"<div class=\"doc-mini-graph\">($b.svg)</div>" })
|
|
let live_html = (if (($b.live?.href? | default "") | is-empty) { "" } else { $"<p><a class=\"doc-nav-btn\" href=\"($b.live.href)\" target=\"_blank\" rel=\"noopener\">↗ (esc ($b.live.label.en? | default 'See it live'))</a></p>" })
|
|
$"($mini_html)($live_html)"
|
|
} else if $t == "kv" {
|
|
let rows = ($b.rows | each {|r| $"<tr><td>(esc $r.k)</td><td>(esc $r.v)</td></tr>" } | str join)
|
|
$"<table class=\"doc-kv\">($rows)</table>"
|
|
} else { "" }
|
|
}
|
|
|
|
def vm-to-html [vm: record] {
|
|
let hero = ($vm.hero.en? | default "")
|
|
let hero_html = (if ($hero | is-empty) { "" } else { $"<p class=\"doc-meta\" style=\"display:block;font-size:1rem\">(esc $hero)</p>" })
|
|
let sections = ($vm.sections | each {|s|
|
|
let body = ($s.blocks | each {|b| vm-block-html $b } | str join "\n")
|
|
(sec $s.key ($s.title.en) $body)
|
|
} | str join "\n")
|
|
$"<div class=\"doc-head\">
|
|
<span class=\"doc-badge doc-tag-operation\">Project</span>
|
|
<span class=\"doc-meta\">(esc $vm.level_id)</span>
|
|
<h1 style=\"margin:.5rem 0 0\">About (esc $vm.level_id)</h1>
|
|
($hero_html)
|
|
</div>
|
|
($sections)"
|
|
}
|
|
|
|
# ── main ──────────────────────────────────────────────────────────────────────
|
|
|
|
def main [
|
|
--root: string = "../.."
|
|
--out: string = "../../outreach/web/about" # static output dir (Option A)
|
|
--emit-json: string = "" # if set, write the view-model JSON here (Option B)
|
|
--daemon: string = "https://ontoref.dev"
|
|
] {
|
|
let pos = $"($root)/.ontoref/positioning"
|
|
let about_path = $"($pos)/about.ncl"
|
|
let ip = $"($root)/code/ontology"
|
|
let daemon = ($daemon | str trim --right --char '/')
|
|
|
|
if not ($about_path | path exists) { error make { msg: $"about.ncl not found: ($about_path)" } }
|
|
let about = (nickel export --format json --import-path $ip $about_path | from json)
|
|
|
|
if ($about.kind != "Project") {
|
|
print $"gen-about-pages: level ($about.level_id) kind=($about.kind) — personal About uses about.j2, skipping"
|
|
return
|
|
}
|
|
|
|
let nodes = (try { (nickel export --format json --import-path $ip $"($root)/.ontoref/ontology/core.ncl" | from json).nodes } catch { [] })
|
|
let cg_path = $"($root)/outreach/site/site/public/r/content_graph.json"
|
|
let mini_graphs = if ($cg_path | path exists) { (open $cg_path).mini } else { {} }
|
|
let map_path = $"($root)/outreach/site/site/public/r/adr-map.json"
|
|
let adr_map = if ($map_path | path exists) { (open $map_path) } else { {} }
|
|
|
|
let vm = (build-vm $about $root $ip $daemon $nodes $adr_map $mini_graphs)
|
|
|
|
if ($emit_json | is-not-empty) {
|
|
let parent = ($emit_json | path dirname)
|
|
if ($parent | is-not-empty) { mkdir $parent }
|
|
$vm | to json --indent 2 | save -f $emit_json
|
|
print $"gen-about-pages: ($vm.level_id) view-model → ($emit_json)"
|
|
} else {
|
|
let dir = $"($out)/($vm.level_id)"
|
|
mkdir $dir
|
|
page-shell $"About ($vm.level_id)" (vm-to-html $vm) | save -f $"($dir)/index.html"
|
|
print $"gen-about-pages: ($vm.level_id) → ($dir)/index.html"
|
|
}
|
|
}
|