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).
313 lines
14 KiB
Text
313 lines
14 KiB
Text
#!/usr/bin/env nu
|
|
|
|
# Project the catalog of verifiables (.ontoref/catalog) into renderable content —
|
|
# sibling of gen-adr-pages.nu. The catalog holds two kinds:
|
|
# operations : typed agent actions (inputs, effects, constraints, witness shape)
|
|
# validators : the criteria that gate them (category, severity, slice reads)
|
|
#
|
|
# Two modes, identical to the ADR surface:
|
|
# static : self-contained brand-wrapped HTML viewer — outreach/web + docs
|
|
# content : markdown per entry into the `catalog` content-kind tree (needs the
|
|
# kind registered in content.ncl/routes.ncl — a rebuild boundary)
|
|
#
|
|
# Page chrome comes from lib/doc-page.nu so ADR + catalog look identical.
|
|
#
|
|
# Usage:
|
|
# nu scripts/build/gen-catalog-pages.nu --root ../.. --mode static --out ../../outreach/web/catalog
|
|
|
|
use lib/doc-page.nu [esc paras sec page-shell]
|
|
|
|
# ── shared small renderers ────────────────────────────────────────────────────
|
|
|
|
def kv-table [rows: list] {
|
|
let body = ($rows | where {|r| ($r.1 | str trim | is-not-empty) }
|
|
| each {|r| $"<tr><td>(esc $r.0)</td><td>(esc $r.1)</td></tr>" } | str join)
|
|
if ($body | is-empty) { "" } else { $"<table class=\"doc-kv\">($body)</table>" }
|
|
}
|
|
|
|
def bool-str [b] { if ($b == true) { "yes" } else if ($b == false) { "no" } else { "" } }
|
|
|
|
# Link a referenced validator id to its catalog page if it resolves to a known
|
|
# entry; otherwise render it as inline code (inline expression refs, unknowns).
|
|
def vref-link [ref, known_ids: list] {
|
|
if (($ref | describe) != "string") { return "<code class=\"doc-code\">inline</code>" }
|
|
if ($ref in $known_ids) {
|
|
$"<a href=\"/catalog/($ref)/\"><code class=\"doc-code\">($ref)</code></a>"
|
|
} else {
|
|
$"<code class=\"doc-code\">(esc $ref)</code>"
|
|
}
|
|
}
|
|
|
|
# ── validator page ────────────────────────────────────────────────────────────
|
|
|
|
def render-validator-html [v: record] {
|
|
let cat = ($v.category? | default "")
|
|
let sev = ($v.severity? | default "")
|
|
let cat_badge = if ($cat | is-not-empty) { $"<span class=\"doc-badge doc-tag-(($cat | str downcase))\">(esc $cat)</span>" } else { "" }
|
|
let sev_badge = if ($sev | is-not-empty) { $"<span class=\"doc-badge doc-sev-(($sev | str downcase))\">(esc $sev)</span>" } else { "" }
|
|
|
|
let meta = (kv-table [
|
|
["criterion" ($v.criterion_ref? | default "")]
|
|
["predicate" ($v.predicate_ref? | default "")]
|
|
["decidable" (bool-str ($v.decidable?))]
|
|
["touches spiral" (bool-str ($v.touches_spiral?))]
|
|
["needs state root" (bool-str ($v.slice?.needs_state_root?))]
|
|
])
|
|
|
|
let reads = ($v.slice?.reads? | default [])
|
|
let reads_html = if ($reads | is-empty) { "" } else {
|
|
let rows = ($reads | each {|r|
|
|
let fields = (($r.fields? | default []) | each {|f| esc $f } | str join ", ")
|
|
$"<li><strong>(esc ($r.entity? | default ''))</strong><div><p class=\"doc-code\">($fields)</p></div></li>"
|
|
} | str join)
|
|
$"<ul class=\"doc-list\">($rows)</ul>"
|
|
}
|
|
|
|
let body = ([
|
|
$"<div class=\"doc-head\">
|
|
<span class=\"doc-badge doc-tag-validator\">Validator</span>($cat_badge)($sev_badge)
|
|
<span class=\"doc-meta\">(esc ($v.id? | default ''))</span>
|
|
<h1 style=\"margin:.5rem 0 0\">(esc ($v.id? | default ''))</h1>
|
|
</div>"
|
|
(sec "description" "Description" (paras ($v.description? | default "")))
|
|
(sec "spec" "Specification" $meta)
|
|
(sec "reads" "Reads (slice)" $reads_html)
|
|
] | str join "\n")
|
|
|
|
page-shell ($v.id? | default "Validator") $body
|
|
}
|
|
|
|
# ── operation page ────────────────────────────────────────────────────────────
|
|
|
|
def render-operation-html [op: record, known_ids: list] {
|
|
let sla = ($op.validation_sla? | default "")
|
|
let sla_str = if (($sla | describe) == "string") { $sla } else if (($sla | describe | str starts-with "record")) { ($sla.tag? | default "") } else { "" }
|
|
|
|
let pre = (kv-table [
|
|
["slice query" ($op.precondition?.slice_query? | default "")]
|
|
["slice kind" ($op.precondition?.slice_kind? | default "")]
|
|
["validation SLA" $sla_str]
|
|
["roles" (($op.actor_policy?.roles? | default []) | str join ", ")]
|
|
])
|
|
|
|
let inputs = ($op.inputs? | default {})
|
|
let inputs_html = if (($inputs | columns | length) == 0) { "" } else {
|
|
let rows = ($inputs | transpose k v | each {|i| $"<tr><td>(esc $i.k)</td><td class=\"doc-code\">(esc $i.v)</td></tr>" } | str join)
|
|
$"<table class=\"doc-kv\">($rows)</table>"
|
|
}
|
|
|
|
let effects = ($op.effects? | default [])
|
|
let effects_html = if ($effects | is-empty) { "" } else {
|
|
let rows = ($effects | each {|e|
|
|
let attrs = (($e.attrs? | default []) | each {|a| esc $a } | str join ", ")
|
|
let dims = (($e.dimensions? | default []) | where {|d| ($d | str trim | is-not-empty) } | each {|d| esc $d } | str join ", ")
|
|
let dim_s = if ($dims | is-empty) { "" } else { $" · dims: ($dims)" }
|
|
$"<li><span class=\"doc-badge doc-sev-soft\">(esc ($e.kind? | default ''))</span><div><p><strong>(esc ($e.entity? | default ''))</strong> — <span class=\"doc-code\">($attrs)</span>($dim_s)</p></div></li>"
|
|
} | str join)
|
|
$"<ul class=\"doc-list\">($rows)</ul>"
|
|
}
|
|
|
|
let pre_v = ($op.constraints?.pre? | default [])
|
|
let post_v = ($op.constraints?.post? | default [])
|
|
let constraints_html = if (($pre_v | is-empty) and ($post_v | is-empty)) { "" } else {
|
|
let pre_links = if ($pre_v | is-empty) { "—" } else { ($pre_v | each {|r| vref-link $r $known_ids } | str join " · ") }
|
|
let post_links = if ($post_v | is-empty) { "—" } else { ($post_v | each {|r| vref-link $r $known_ids } | str join " · ") }
|
|
$"<table class=\"doc-kv\"><tr><td>pre</td><td>($pre_links)</td></tr><tr><td>post</td><td>($post_links)</td></tr></table>"
|
|
}
|
|
|
|
let ws = ($op.witness_shape? | default {})
|
|
let witness_html = if (($ws | columns | length) == 0) { "" } else {
|
|
let proofs = (($ws.proof_paths? | default []) | each {|p| esc $p } | str join "<br>")
|
|
(kv-table [
|
|
["payload kind" ($ws.payload_kind? | default "")]
|
|
["proof paths" ""]
|
|
] | str replace '<td>proof paths</td><td></td>' $"<td>proof paths</td><td class=\"doc-code\">($proofs)</td>")
|
|
}
|
|
|
|
let renders = ($op.render_paths? | default [])
|
|
let render_html = if ($renders | is-empty) { "" } else {
|
|
let items = ($renders | each {|p| $"<li><span class=\"doc-code\">(esc $p)</span></li>" } | str join)
|
|
$"<ul class=\"doc-list\">($items)</ul>"
|
|
}
|
|
|
|
let sla_badge = if ($sla_str | is-not-empty) { $"<span class=\"doc-badge doc-sev-soft\">(esc $sla_str)</span>" } else { "" }
|
|
let body = ([
|
|
$"<div class=\"doc-head\">
|
|
<span class=\"doc-badge doc-tag-operation\">Operation</span>($sla_badge)
|
|
<span class=\"doc-meta\">(esc ($op.id? | default ''))</span>
|
|
<h1 style=\"margin:.5rem 0 0\">(esc ($op.id? | default ''))</h1>
|
|
</div>"
|
|
(sec "description" "Description" (paras ($op.description? | default "")))
|
|
(sec "preconditions" "Preconditions & policy" $pre)
|
|
(sec "inputs" "Inputs" $inputs_html)
|
|
(sec "effects" "Effects" $effects_html)
|
|
(sec "constraints" "Validators" $constraints_html)
|
|
(sec "witness" "Witness shape" $witness_html)
|
|
(sec "renders" "Render paths" $render_html)
|
|
] | str join "\n")
|
|
|
|
page-shell ($op.id? | default "Operation") $body
|
|
}
|
|
|
|
# ── index ──────────────────────────────────────────────────────────────────────
|
|
|
|
def render-index [ops: list, validators: list] {
|
|
let op_search = ($ops | each {|o|
|
|
let id = ($o.id? | default "")
|
|
let desc = ($o.description? | default "" | str trim | str substring 0..400 | str replace --all "'" "\\'" | str replace --all "\n" " ")
|
|
$" \{id:'($id)',kind:'operation',text:'($desc)'\}"
|
|
})
|
|
let v_search = ($validators | each {|v|
|
|
let id = ($v.id? | default "")
|
|
let desc = ($v.description? | default "" | str trim | str substring 0..400 | str replace --all "'" "\\'" | str replace --all "\n" " ")
|
|
$" \{id:'($id)',kind:'validator',text:'($desc)'\}"
|
|
})
|
|
|
|
let op_rows = ($ops | sort-by id | each {|o|
|
|
$"<li data-id=\"($o.id? | default '')\" data-title=\"(esc ($o.id? | default ''))\" data-text=\"(esc ($o.description? | default '' | str trim | str substring 0..300))\"><a href=\"/catalog/($o.id? | default '')/\"><code>op</code>(esc ($o.id? | default ''))</a></li>"
|
|
} | str join)
|
|
let v_rows = ($validators | sort-by id | each {|v|
|
|
$"<li data-id=\"($v.id? | default '')\" data-title=\"(esc ($v.id? | default ''))\" data-text=\"(esc ($v.description? | default '' | str trim | str substring 0..300))\"><a href=\"/catalog/($v.id? | default '')/\"><code>val</code>(esc ($v.id? | default ''))</a></li>"
|
|
} | str join)
|
|
|
|
let blocks = $"<div class=\"doc-idx-group\" data-status=\"Operations\"><h2>Operations \(($ops | length)\)</h2><ul class=\"doc-idx-list\">($op_rows)</ul></div><div class=\"doc-idx-group\" data-status=\"Validators\"><h2>Validators \(($validators | length)\)</h2><ul class=\"doc-idx-list\">($v_rows)</ul></div>"
|
|
|
|
let search_js = $"<script>
|
|
var searchInput=document.getElementById\('doc-search'\);
|
|
var noResults=document.getElementById\('no-results'\);
|
|
function doSearch\(\){
|
|
var q=searchInput.value.toLowerCase\(\).trim\(\);
|
|
var hits=0;
|
|
document.querySelectorAll\('.doc-idx-list>li'\).forEach\(function\(li\){
|
|
var show=!q||li.dataset.title.toLowerCase\(\).indexOf\(q\)>-1||li.dataset.text.toLowerCase\(\).indexOf\(q\)>-1;
|
|
li.classList.toggle\('hidden',!show\);
|
|
if\(show\)hits++;
|
|
}\);
|
|
document.querySelectorAll\('.doc-idx-group'\).forEach\(function\(g\){
|
|
var visible=g.querySelectorAll\('li:not\(.hidden\)'\).length>0;
|
|
g.style.display=visible?'':'none';
|
|
}\);
|
|
noResults.style.display=hits===0&&q?'block':'none';
|
|
}
|
|
searchInput.addEventListener\('input',doSearch\);
|
|
</script>"
|
|
|
|
let content = $"<h1 style=\"margin:0 0 .5rem\">Catalog of Verifiables</h1>
|
|
<p class=\"doc-meta\" style=\"margin:0 0 1rem;display:block\">Typed agent operations and the validators that gate them \(ADR-024 / ADR-026 / ADR-050\).</p>
|
|
<input id=\"doc-search\" class=\"doc-search\" type=\"search\" placeholder=\"Search catalog by id or description…\">
|
|
<p id=\"no-results\" class=\"no-results\">Nothing in the catalog matches your search.</p>
|
|
($blocks)
|
|
($search_js)"
|
|
|
|
page-shell "Catalog of Verifiables" $content
|
|
}
|
|
|
|
# ── content-kind sidecar (for the future `catalog` kind + content_graph) ──────
|
|
|
|
def render-catalog-ncl [id: string, kind: string, title: string, related: list] {
|
|
let rel = ($related | uniq | where {|k| $k != $id } | each {|k| $"\"($k)\"" } | str join ", ")
|
|
$"{
|
|
id = \"($id)\",
|
|
title = \"($title)\",
|
|
slug = \"($id)\",
|
|
excerpt = \"\",
|
|
tags = [\"catalog\", \"($kind)\"],
|
|
page_route = \"/catalog/($id)\",
|
|
graph = {
|
|
implements = [],
|
|
related_to = [($rel)],
|
|
},
|
|
}
|
|
"
|
|
}
|
|
|
|
def render-md [id: string, kind: string, description: string] {
|
|
let excerpt = ($description | str trim | str substring 0..200 | str replace --all '"' "'")
|
|
$"---
|
|
id: \"($id)\"
|
|
title: \"($id)\"
|
|
slug: \"($id)\"
|
|
subtitle: \"($kind)\"
|
|
excerpt: \"($excerpt)\"
|
|
author: \"ontoref\"
|
|
published: true
|
|
featured: false
|
|
category: \"($kind)\"
|
|
tags: [\"($id)\", \"catalog\", \"($kind)\"]
|
|
css_class: \"category-catalog\"
|
|
---
|
|
|
|
<h2>Description</h2>
|
|
|
|
(paras $description)
|
|
"
|
|
}
|
|
|
|
# ── main ──────────────────────────────────────────────────────────────────────
|
|
|
|
def main [
|
|
--root: string = "../.."
|
|
--mode: string = "static" # static | content | both
|
|
--out: string = "site/public/catalog" # static output dir
|
|
--content-out: string = "site/content/catalog/en" # content-kind output
|
|
] {
|
|
let cat_dir = $"($root)/.ontoref/catalog"
|
|
let ip_data = $"($root)/code/ontology"
|
|
let do_static = ($mode in ["static" "both"])
|
|
let do_content = ($mode in ["content" "both"])
|
|
|
|
let op_files = (glob $"($cat_dir)/operations/*.ncl" | where {|f| ($f | path basename) not-in ["schema.ncl" "_template.ncl"] } | sort)
|
|
let v_files = (glob $"($cat_dir)/validators/*.ncl" | where {|f| ($f | path basename) != "schema.ncl" } | sort)
|
|
|
|
mut ops = []
|
|
for f in $op_files {
|
|
let op = (try { nickel export --format json --import-path $ip_data --import-path $cat_dir $f | from json } catch {|e| print $" skip ($f | path basename): ($e.msg)"; null })
|
|
if ($op != null) { $ops = ($ops | append $op) }
|
|
}
|
|
mut validators = []
|
|
for f in $v_files {
|
|
let v = (try { nickel export --format json --import-path $ip_data --import-path $cat_dir $f | from json } catch {|e| print $" skip ($f | path basename): ($e.msg)"; null })
|
|
if ($v != null) { $validators = ($validators | append $v) }
|
|
}
|
|
|
|
let known_ids = ($validators | each {|v| $v.id? | default "" } | where {|x| $x | is-not-empty })
|
|
|
|
if $do_static {
|
|
for op in $ops {
|
|
let id = ($op.id? | default "")
|
|
if ($id | is-empty) { continue }
|
|
let dir = $"($out)/($id)"
|
|
mkdir $dir
|
|
render-operation-html $op $known_ids | save -f $"($dir)/index.html"
|
|
}
|
|
for v in $validators {
|
|
let id = ($v.id? | default "")
|
|
if ($id | is-empty) { continue }
|
|
let dir = $"($out)/($id)"
|
|
mkdir $dir
|
|
render-validator-html $v | save -f $"($dir)/index.html"
|
|
}
|
|
mkdir $out
|
|
render-index $ops $validators | save -f $"($out)/index.html"
|
|
}
|
|
|
|
if $do_content {
|
|
for op in $ops {
|
|
let id = ($op.id? | default "")
|
|
if ($id | is-empty) { continue }
|
|
mkdir $"($content_out)/operations"
|
|
let related = (($op.constraints?.pre? | default []) | append ($op.constraints?.post? | default []) | where {|r| ($r | describe) == "string" })
|
|
render-md $id "operations" ($op.description? | default "") | save -f $"($content_out)/operations/($id).md"
|
|
render-catalog-ncl $id "operation" $id $related | save -f $"($content_out)/operations/($id).ncl"
|
|
}
|
|
for v in $validators {
|
|
let id = ($v.id? | default "")
|
|
if ($id | is-empty) { continue }
|
|
mkdir $"($content_out)/validators"
|
|
render-md $id "validators" ($v.description? | default "") | save -f $"($content_out)/validators/($id).md"
|
|
render-catalog-ncl $id "validator" $id [] | save -f $"($content_out)/validators/($id).ncl"
|
|
}
|
|
}
|
|
|
|
print $"gen-catalog-pages [($mode)]: ($ops | length) operations, ($validators | length) validators"
|
|
}
|