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).
124 lines
5.4 KiB
Text
124 lines
5.4 KiB
Text
#!/usr/bin/env nu
|
|
|
|
# Project the ontology (core.ncl) into a standalone node-link GRAPH diagram SVG
|
|
# and save it as an image file in the site. This is the web-usable form of
|
|
# `onre diagram`: the 55 nodes laid out on a circle (grouped by level), with the
|
|
# 144 edges drawn as chords across it — a real graph, not a list. Served at
|
|
# /images/ontoref-diagram.svg, referenced by the About graph block.
|
|
#
|
|
# Rendered as an <img> (isolated from page CSS), so colours are baked literal
|
|
# (the ontoref dark palette) rather than CSS vars.
|
|
#
|
|
# Usage (from outreach/site):
|
|
# nu scripts/build/gen-diagram.nu --root ../.. --out site/public/images/ontoref-diagram.svg
|
|
|
|
const TAU = 6.283185307179586
|
|
const DEG = 57.29577951308232
|
|
|
|
# Level order around the ring + per-level colour and dot radius.
|
|
const LEVELS = [
|
|
[level color dot];
|
|
["Axiom" "#E8A838" 6.0]
|
|
["Tension" "#C0CCD8" 6.0]
|
|
["Practice" "#7fc8e0" 4.5]
|
|
]
|
|
|
|
def svg-esc [s: string] {
|
|
$s | str replace --all '&' '&' | str replace --all '<' '<' | str replace --all '>' '>'
|
|
}
|
|
|
|
def clip [s: string, n: int] {
|
|
if (($s | str length) <= $n) { $s } else { (($s | str substring 0..($n - 1)) | str trim) + "…" }
|
|
}
|
|
|
|
def r1 [x] { $x | into float | math round --precision 1 }
|
|
|
|
def main [
|
|
--root: string = "../.."
|
|
--out: string = "site/public/images/ontoref-diagram.svg"
|
|
] {
|
|
let ip = $"($root)/code/ontology"
|
|
let core = $"($root)/.ontoref/ontology/core.ncl"
|
|
if not ($core | path exists) { error make { msg: $"core.ncl not found: ($core)" } }
|
|
let graph = (nickel export --format json --import-path $ip $core | from json)
|
|
let nodes = ($graph | get nodes)
|
|
let edges = ($graph.edges? | default [])
|
|
|
|
# Order nodes by level (contiguous arcs: axioms, then tensions, then practices).
|
|
let ordered = ($LEVELS | get level | each {|lvl|
|
|
$nodes | where {|n| ($n.level? | default "") == $lvl }
|
|
} | flatten)
|
|
let count = ($ordered | length)
|
|
let n_edges = ($edges | length)
|
|
|
|
let W = 920.0
|
|
let cx = ($W / 2)
|
|
let cy = (($W / 2) + 14) # nudge down, leave the title band clear
|
|
let R = 322.0 # node ring radius (labels radiate outside it)
|
|
|
|
# Position every node on the ring; keep id → {x,y,ang} for edge chords.
|
|
let placed = ($ordered | enumerate | each {|e|
|
|
let ang = ($TAU * $e.index / $count)
|
|
let lvl = ($e.item.level? | default "")
|
|
let spec = ($LEVELS | where level == $lvl | first)
|
|
{
|
|
id: ($e.item.id? | default ""),
|
|
name: ($e.item.name? | default ($e.item.id? | default "")),
|
|
level: $lvl,
|
|
color: $spec.color,
|
|
dot: $spec.dot,
|
|
ang: $ang,
|
|
x: ($cx + $R * ($ang | math cos)),
|
|
y: ($cy + $R * ($ang | math sin)),
|
|
}
|
|
})
|
|
let pmap = ($placed | reduce --fold {} {|it, acc| $acc | insert $it.id $it })
|
|
|
|
# Edge chords — only between two placed nodes (skip edges to ADR/project ids).
|
|
let edge_svg = ($edges | each {|ed|
|
|
let a = (try { $pmap | get ($ed.from? | default "") } catch { null })
|
|
let b = (try { $pmap | get ($ed.to? | default "") } catch { null })
|
|
if ($a == null) or ($b == null) { null } else {
|
|
$"<line x1=\"(r1 $a.x)\" y1=\"(r1 $a.y)\" x2=\"(r1 $b.x)\" y2=\"(r1 $b.y)\" stroke=\"#46607c\" stroke-width=\"0.9\" opacity=\"0.30\"/>"
|
|
}
|
|
} | compact | str join "\n ")
|
|
|
|
# Nodes — dot + radial label (rotated to read outward, flipped on the left half).
|
|
let node_svg = ($placed | each {|p|
|
|
let dot = $"<circle cx=\"(r1 $p.x)\" cy=\"(r1 $p.y)\" r=\"($p.dot)\" fill=\"($p.color)\" stroke=\"#0f1319\" stroke-width=\"1.4\"/>"
|
|
let deg = ($p.ang * $DEG)
|
|
let left = (($p.ang | math cos) < 0)
|
|
let lx = ($cx + ($R + 10) * ($p.ang | math cos))
|
|
let ly = ($cy + ($R + 10) * ($p.ang | math sin))
|
|
let rot = (if $left { $deg + 180 } else { $deg })
|
|
let anchor = (if $left { "end" } else { "start" })
|
|
let txt = (clip $p.name 26)
|
|
let label = $"<text x=\"(r1 $lx)\" y=\"(r1 $ly)\" fill=\"#aeb9c8\" font-size=\"9\" font-family=\"Inter, sans-serif\" text-anchor=\"($anchor)\" transform=\"rotate\((r1 $rot) (r1 $lx) (r1 $ly)\)\" dominant-baseline=\"middle\">(svg-esc $txt)</text>"
|
|
$"($dot)\n ($label)"
|
|
} | str join "\n ")
|
|
|
|
# Legend (level → colour + count).
|
|
let legend = ($LEVELS | get level | enumerate | each {|e|
|
|
let lvl = $e.item
|
|
let spec = ($LEVELS | where level == $lvl | first)
|
|
let c = ($placed | where level == $lvl | length)
|
|
let ly = (64 + $e.index * 20)
|
|
$"<circle cx=\"24\" cy=\"(r1 $ly)\" r=\"5\" fill=\"($spec.color)\"/><text x=\"36\" y=\"(r1 ($ly + 4))\" fill=\"#aeb9c8\" font-size=\"12\" font-family=\"Inter, sans-serif\">($lvl)s · ($c)</text>"
|
|
} | str join "\n ")
|
|
|
|
let H = ($W + 4)
|
|
let title = $"<text x=\"24\" y=\"34\" fill=\"#E8A838\" font-size=\"17\" font-weight=\"700\" font-family=\"ui-monospace, monospace\">ontoref</text><text x=\"108\" y=\"34\" fill=\"#8090A4\" font-size=\"13\" font-family=\"Inter, sans-serif\">ontology graph · ($count) nodes · ($n_edges) edges</text>"
|
|
|
|
let svg = $"<svg viewBox=\"0 0 ($W) ($H)\" width=\"($W)\" height=\"($H)\" xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" aria-label=\"ontoref ontology graph — ($count) nodes, ($n_edges) edges\">
|
|
<rect x=\"0.5\" y=\"0.5\" width=\"(r1 ($W - 1))\" height=\"(r1 ($H - 1))\" rx=\"12\" fill=\"#0f1319\" stroke=\"#1e2a38\"/>
|
|
($title)
|
|
($legend)
|
|
($edge_svg)
|
|
($node_svg)
|
|
</svg>"
|
|
|
|
let parent = ($out | path dirname)
|
|
if ($parent | is-not-empty) { mkdir $parent }
|
|
$svg | save -f $out
|
|
print $"gen-diagram: ($count) nodes · ($n_edges) edges → ($out)"
|
|
}
|