site: materialize the outreach repo's site projection, with witnesses

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).
This commit is contained in:
Jesús Pérez 2026-07-11 23:44:00 +01:00
parent 5619a40d4f
commit 72a389f056
857 changed files with 115438 additions and 0 deletions

28
site/.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
# ── Secrets ──────────────────────────────────────────────────────────────────
# outreach is mirrored to a PUBLIC github remote (ADR-062 forge rule), so a leak
# here is a publication, not a mistake. Both files are real keys, not templates.
.env
.k
# ── Generated: content indexes ───────────────────────────────────────────────
# site/r is written by content_processor (canonical serve tree); site/public/r is
# the deploy mirror. Both are reproducible from site/content via `just content`.
site/r/
site/public/r/
# ── Generated: deploy packs ──────────────────────────────────────────────────
# 228 MB of dated tarballs. They are the only forensic record of what shipped —
# valuable, but they are artifacts, not source. Keep them local.
provisioning/.content-packs/
# ── Generated: runtime + build outputs ───────────────────────────────────────
# htmx-templates/ and htmx-assets/ are ASSEMBLED (`just templates` rm -rf's them).
# They stay tracked until templates-overlay/ is the declared source for every
# hand-authored template and `just templates-check` proves regeneration is a
# no-op — ignoring a generated tree that still holds unique work loses it.
cache/
data/
logs_*.out
.wrks/
works-pv/

1
site/.just/content.just Symbolic link
View file

@ -0,0 +1 @@
../../../../website-htmx-rustelo/code/justfiles/content.just

View file

@ -0,0 +1,147 @@
//==========================================================
// head-support.js
//
// An extension to htmx 1.0 to add head tag merging.
//==========================================================
(function(){
var api = null;
function log() {
//console.log(arguments);
}
function mergeHead(newContent, defaultMergeStrategy) {
if (newContent && newContent.indexOf('<head') > -1) {
const htmlDoc = document.createElement("html");
// remove svgs to avoid conflicts
var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
// extract head tag
var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
// if the head tag exists...
if (headTag) {
var added = []
var removed = []
var preserved = []
var nodesToAppend = []
htmlDoc.innerHTML = headTag;
var newHeadTag = htmlDoc.querySelector("head");
var currentHead = document.head;
if (newHeadTag == null) {
return;
} else {
// put all new head elements into a Map, by their outerHTML
var srcToNewHeadNodes = new Map();
for (const newHeadChild of newHeadTag.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
}
}
// determine merge strategy
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
// get the current head
for (const currentHeadElt of currentHead.children) {
// If the current head element is in the map
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
if (inNewContent || isPreserved) {
if (isReAppended) {
// remove the current version and let the new version replace it and re-execute
removed.push(currentHeadElt);
} else {
// this element already exists and should not be re-appended, so remove it from
// the new content map, preserving it in the DOM
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
preserved.push(currentHeadElt);
}
} else {
if (mergeStrategy === "append") {
// we are appending and this existing element is not new content
// so if and only if it is marked for re-append do we do anything
if (isReAppended) {
removed.push(currentHeadElt);
nodesToAppend.push(currentHeadElt);
}
} else {
// if this is a merge, we remove this content since it is not in the new head
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
removed.push(currentHeadElt);
}
}
}
}
// Push the tremaining new head elements in the Map into the
// nodes to append to the head tag
nodesToAppend.push(...srcToNewHeadNodes.values());
log("to append: ", nodesToAppend);
for (const newNode of nodesToAppend) {
log("adding: ", newNode);
var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
log(newElt);
if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
currentHead.appendChild(newElt);
added.push(newElt);
}
}
// remove all removed elements, after we have appended the new elements to avoid
// additional network requests for things like style sheets
for (const removedElement of removed) {
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
currentHead.removeChild(removedElement);
}
}
api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
}
}
}
htmx.defineExtension("head-support", {
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
htmx.on('htmx:afterSwap', function(evt){
// PATCH: htmx:afterSwap also fires on history restore from
// popstate, where evt.detail.xhr is undefined. The original
// line would TypeError and block subsequent navigations.
// History restores have their own handler below
// (htmx:historyRestore), so we just skip here.
if (!evt.detail || !evt.detail.xhr) return;
var serverResponse = evt.detail.xhr.response;
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
}
})
htmx.on('htmx:historyRestore', function(evt){
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
if (evt.detail.cacheMiss) {
mergeHead(evt.detail.serverResponse, "merge");
} else {
mergeHead(evt.detail.item.head, "merge");
}
}
})
htmx.on('htmx:historyItemCreated', function(evt){
var historyItem = evt.detail.item;
historyItem.head = document.head.outerHTML;
})
}
});
})()

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,184 @@
;(function() {
const loadingStatesUndoQueue = []
function loadingStateContainer(target) {
return htmx.closest(target, '[data-loading-states]') || document.body
}
function mayProcessUndoCallback(target, callback) {
if (document.body.contains(target)) {
callback()
}
}
function mayProcessLoadingStateByPath(elt, requestPath) {
const pathElt = htmx.closest(elt, '[data-loading-path]')
if (!pathElt) {
return true
}
return pathElt.getAttribute('data-loading-path') === requestPath
}
function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) {
const delayElt = htmx.closest(sourceElt, '[data-loading-delay]')
if (delayElt) {
const delayInMilliseconds =
delayElt.getAttribute('data-loading-delay') || 200
const timeout = setTimeout(function() {
doCallback()
loadingStatesUndoQueue.push(function() {
mayProcessUndoCallback(targetElt, undoCallback)
})
}, delayInMilliseconds)
loadingStatesUndoQueue.push(function() {
mayProcessUndoCallback(targetElt, function() { clearTimeout(timeout) })
})
} else {
doCallback()
loadingStatesUndoQueue.push(function() {
mayProcessUndoCallback(targetElt, undoCallback)
})
}
}
function getLoadingStateElts(loadingScope, type, path) {
return Array.from(htmx.findAll(loadingScope, '[' + type + ']')).filter(
function(elt) { return mayProcessLoadingStateByPath(elt, path) }
)
}
function getLoadingTarget(elt) {
if (elt.getAttribute('data-loading-target')) {
return Array.from(
htmx.findAll(elt.getAttribute('data-loading-target'))
)
}
return [elt]
}
htmx.defineExtension('loading-states', {
onEvent: function(name, evt) {
if (name === 'htmx:beforeRequest') {
const container = loadingStateContainer(evt.target)
const loadingStateTypes = [
'data-loading',
'data-loading-class',
'data-loading-class-remove',
'data-loading-disable',
'data-loading-aria-busy'
]
const loadingStateEltsByType = {}
loadingStateTypes.forEach(function(type) {
loadingStateEltsByType[type] = getLoadingStateElts(
container,
type,
evt.detail.pathInfo.requestPath
)
})
loadingStateEltsByType['data-loading'].forEach(function(sourceElt) {
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() {
targetElt.style.display =
sourceElt.getAttribute('data-loading') ||
'inline-block'
},
function() { targetElt.style.display = 'none' }
)
})
})
loadingStateEltsByType['data-loading-class'].forEach(
function(sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class')
.split(' ')
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() {
classNames.forEach(function(className) {
targetElt.classList.add(className)
})
},
function() {
classNames.forEach(function(className) {
targetElt.classList.remove(className)
})
}
)
})
}
)
loadingStateEltsByType['data-loading-class-remove'].forEach(
function(sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class-remove')
.split(' ')
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() {
classNames.forEach(function(className) {
targetElt.classList.remove(className)
})
},
function() {
classNames.forEach(function(className) {
targetElt.classList.add(className)
})
}
)
})
}
)
loadingStateEltsByType['data-loading-disable'].forEach(
function(sourceElt) {
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() { targetElt.disabled = true },
function() { targetElt.disabled = false }
)
})
}
)
loadingStateEltsByType['data-loading-aria-busy'].forEach(
function(sourceElt) {
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() { targetElt.setAttribute('aria-busy', 'true') },
function() { targetElt.removeAttribute('aria-busy') }
)
})
}
)
}
if (name === 'htmx:beforeOnLoad') {
while (loadingStatesUndoQueue.length > 0) {
loadingStatesUndoQueue.shift()()
}
}
}
})
})()

View file

@ -0,0 +1,44 @@
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api
htmx.defineExtension('multi-swap', {
init: function(apiRef) {
api = apiRef
},
isInlineSwap: function(swapStyle) {
return swapStyle.indexOf('multi:') === 0
},
handleSwap: function(swapStyle, target, fragment, settleInfo) {
if (swapStyle.indexOf('multi:') === 0) {
var selectorToSwapStyle = {}
var elements = swapStyle.replace(/^multi\s*:\s*/, '').split(/\s*,\s*/)
elements.forEach(function(element) {
var split = element.split(/\s*:\s*/)
var elementSelector = split[0]
var elementSwapStyle = typeof (split[1]) !== 'undefined' ? split[1] : 'innerHTML'
if (elementSelector.charAt(0) !== '#') {
console.error("HTMX multi-swap: unsupported selector '" + elementSelector + "'. Only ID selectors starting with '#' are supported.")
return
}
selectorToSwapStyle[elementSelector] = elementSwapStyle
})
for (var selector in selectorToSwapStyle) {
var swapStyle = selectorToSwapStyle[selector]
var elementToSwap = fragment.querySelector(selector)
if (elementToSwap) {
api.oobSwap(swapStyle, elementToSwap, settleInfo)
} else {
console.warn("HTMX multi-swap: selector '" + selector + "' not found in source content.")
}
}
return true
}
}
})
})()

View file

@ -0,0 +1,59 @@
(function() {
'use strict'
// Save a reference to the global object (window in the browser)
var _root = this
function dependsOn(pathSpec, url) {
if (pathSpec === 'ignore') {
return false
}
var dependencyPath = pathSpec.split('/')
var urlPath = url.split('/')
for (var i = 0; i < urlPath.length; i++) {
var dependencyElement = dependencyPath.shift()
var pathElement = urlPath[i]
if (dependencyElement !== pathElement && dependencyElement !== '*') {
return false
}
if (dependencyPath.length === 0 || (dependencyPath.length === 1 && dependencyPath[0] === '')) {
return true
}
}
return false
}
function refreshPath(path) {
var eltsWithDeps = htmx.findAll('[path-deps]')
for (var i = 0; i < eltsWithDeps.length; i++) {
var elt = eltsWithDeps[i]
if (dependsOn(elt.getAttribute('path-deps'), path)) {
htmx.trigger(elt, 'path-deps')
}
}
}
htmx.defineExtension('path-deps', {
onEvent: function(name, evt) {
if (name === 'htmx:beforeOnLoad') {
var config = evt.detail.requestConfig
// mutating call
if (config.verb !== 'get' && evt.target.getAttribute('path-deps') !== 'ignore') {
refreshPath(config.path)
}
}
}
})
/**
* ********************
* Expose functionality
* ********************
*/
_root.PathDeps = {
refresh: function(path) {
refreshPath(path)
}
}
}).call(this)

View file

@ -0,0 +1,140 @@
// This adds the "preload" extension to htmx. By default, this will
// preload the targets of any tags with `href` or `hx-get` attributes
// if they also have a `preload` attribute as well. See documentation
// for more details
htmx.defineExtension('preload', {
onEvent: function(name, event) {
// Only take actions on "htmx:afterProcessNode"
if (name !== 'htmx:afterProcessNode') {
return
}
// SOME HELPER FUNCTIONS WE'LL NEED ALONG THE WAY
// attr gets the closest non-empty value from the attribute.
var attr = function(node, property) {
if (node == undefined) { return undefined }
return node.getAttribute(property) || node.getAttribute('data-' + property) || attr(node.parentElement, property)
}
// load handles the actual HTTP fetch, and uses htmx.ajax in cases where we're
// preloading an htmx resource (this sends the same HTTP headers as a regular htmx request)
var load = function(node) {
// Called after a successful AJAX request, to mark the
// content as loaded (and prevent additional AJAX calls.)
var done = function(html) {
if (!node.preloadAlways) {
node.preloadState = 'DONE'
}
if (attr(node, 'preload-images') == 'true') {
document.createElement('div').innerHTML = html // create and populate a node to load linked resources, too.
}
}
return function() {
// If this value has already been loaded, then do not try again.
if (node.preloadState !== 'READY') {
return
}
// Special handling for HX-GET - use built-in htmx.ajax function
// so that headers match other htmx requests, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future
var hxGet = node.getAttribute('hx-get') || node.getAttribute('data-hx-get')
if (hxGet) {
htmx.ajax('GET', hxGet, {
source: node,
handler: function(elt, info) {
done(info.xhr.responseText)
}
})
return
}
// Otherwise, perform a standard xhr request, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future.
if (node.getAttribute('href')) {
var r = new XMLHttpRequest()
r.open('GET', node.getAttribute('href'))
r.onload = function() { done(r.responseText) }
r.send()
}
}
}
// This function processes a specific node and sets up event handlers.
// We'll search for nodes and use it below.
var init = function(node) {
// If this node DOES NOT include a "GET" transaction, then there's nothing to do here.
if (node.getAttribute('href') + node.getAttribute('hx-get') + node.getAttribute('data-hx-get') == '') {
return
}
// Guarantee that we only initialize each node once.
if (node.preloadState !== undefined) {
return
}
// Get event name from config.
var on = attr(node, 'preload') || 'mousedown'
const always = on.indexOf('always') !== -1
if (always) {
on = on.replace('always', '').trim()
}
// FALL THROUGH to here means we need to add an EventListener
// Apply the listener to the node
node.addEventListener(on, function(evt) {
if (node.preloadState === 'PAUSE') { // Only add one event listener
node.preloadState = 'READY' // Required for the `load` function to trigger
// Special handling for "mouseover" events. Wait 100ms before triggering load.
if (on === 'mouseover') {
window.setTimeout(load(node), 100)
} else {
load(node)() // all other events trigger immediately.
}
}
})
// Special handling for certain built-in event handlers
switch (on) {
case 'mouseover':
// Mirror `touchstart` events (fires immediately)
node.addEventListener('touchstart', load(node))
// WHhen the mouse leaves, immediately disable the preload
node.addEventListener('mouseout', function(evt) {
if ((evt.target === node) && (node.preloadState === 'READY')) {
node.preloadState = 'PAUSE'
}
})
break
case 'mousedown':
// Mirror `touchstart` events (fires immediately)
node.addEventListener('touchstart', load(node))
break
}
// Mark the node as ready to run.
node.preloadState = 'PAUSE'
node.preloadAlways = always
htmx.trigger(node, 'preload:init') // This event can be used to load content immediately.
}
// Search for all child nodes that have a "preload" attribute
event.target.querySelectorAll('[preload]').forEach(function(node) {
// Initialize the node with the "preload" attribute
init(node)
// Initialize all child elements that are anchors or have `hx-get` (use with care)
node.querySelectorAll('a,[hx-get],[data-hx-get]').forEach(init)
})
}
})

View file

@ -0,0 +1,129 @@
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api
var attrPrefix = 'hx-target-'
// IE11 doesn't support string.startsWith
function startsWith(str, prefix) {
return str.substring(0, prefix.length) === prefix
}
/**
* @param {HTMLElement} elt
* @param {number} respCode
* @returns {HTMLElement | null}
*/
function getRespCodeTarget(elt, respCodeNumber) {
if (!elt || !respCodeNumber) return null
var respCode = respCodeNumber.toString()
// '*' is the original syntax, as the obvious character for a wildcard.
// The 'x' alternative was added for maximum compatibility with HTML
// templating engines, due to ambiguity around which characters are
// supported in HTML attributes.
//
// Start with the most specific possible attribute and generalize from
// there.
var attrPossibilities = [
respCode,
respCode.substr(0, 2) + '*',
respCode.substr(0, 2) + 'x',
respCode.substr(0, 1) + '*',
respCode.substr(0, 1) + 'x',
respCode.substr(0, 1) + '**',
respCode.substr(0, 1) + 'xx',
'*',
'x',
'***',
'xxx'
]
if (startsWith(respCode, '4') || startsWith(respCode, '5')) {
attrPossibilities.push('error')
}
for (var i = 0; i < attrPossibilities.length; i++) {
var attr = attrPrefix + attrPossibilities[i]
var attrValue = api.getClosestAttributeValue(elt, attr)
if (attrValue) {
if (attrValue === 'this') {
return api.findThisElement(elt, attr)
} else {
return api.querySelectorExt(elt, attrValue)
}
}
}
return null
}
/** @param {Event} evt */
function handleErrorFlag(evt) {
if (evt.detail.isError) {
if (htmx.config.responseTargetUnsetsError) {
evt.detail.isError = false
}
} else if (htmx.config.responseTargetSetsError) {
evt.detail.isError = true
}
}
htmx.defineExtension('response-targets', {
/** @param {import("../htmx").HtmxInternalApi} apiRef */
init: function(apiRef) {
api = apiRef
if (htmx.config.responseTargetUnsetsError === undefined) {
htmx.config.responseTargetUnsetsError = true
}
if (htmx.config.responseTargetSetsError === undefined) {
htmx.config.responseTargetSetsError = false
}
if (htmx.config.responseTargetPrefersExisting === undefined) {
htmx.config.responseTargetPrefersExisting = false
}
if (htmx.config.responseTargetPrefersRetargetHeader === undefined) {
htmx.config.responseTargetPrefersRetargetHeader = true
}
},
/**
* @param {string} name
* @param {Event} evt
*/
onEvent: function(name, evt) {
if (name === 'htmx:beforeSwap' &&
evt.detail.xhr &&
evt.detail.xhr.status !== 200) {
if (evt.detail.target) {
if (htmx.config.responseTargetPrefersExisting) {
evt.detail.shouldSwap = true
handleErrorFlag(evt)
return true
}
if (htmx.config.responseTargetPrefersRetargetHeader &&
evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) {
evt.detail.shouldSwap = true
handleErrorFlag(evt)
return true
}
}
if (!evt.detail.requestConfig) {
return true
}
var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status)
if (target) {
handleErrorFlag(evt)
evt.detail.shouldSwap = true
evt.detail.target = target
}
return true
}
}
})
})()

301
site/htmx-assets/ext/sse.js Normal file
View file

@ -0,0 +1,301 @@
/*
Server Sent Events Extension
============================
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
*/
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api
htmx.defineExtension('sse', {
/**
* Init saves the provided reference to the internal HTMX API.
*
* @param {import("../htmx").HtmxInternalApi} api
* @returns void
*/
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
var parent = evt.target || evt.detail.elt
switch (name) {
case 'htmx:beforeCleanupElement':
var internalData = api.getInternalData(parent)
// Try to remove remove an EventSource when elements are removed
if (internalData.sseEventSource) {
internalData.sseEventSource.close()
}
return
// Try to create EventSources when elements are processed
case 'htmx:afterProcessNode':
ensureEventSourceOnElement(parent)
}
}
})
/// ////////////////////////////////////////////
// HELPER FUNCTIONS
/// ////////////////////////////////////////////
/**
* createEventSource is the default method for creating new EventSource objects.
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
*
* @param {string} url
* @returns EventSource
*/
function createEventSource(url) {
return new EventSource(url, { withCredentials: true })
}
/**
* registerSSE looks for attributes that can contain sse events, right
* now hx-trigger and sse-swap and adds listeners based on these attributes too
* the closest event source
*
* @param {HTMLElement} elt
*/
function registerSSE(elt) {
// Add message handlers for every `sse-swap` attribute
queryAttributeOnThisOrChildren(elt, 'sse-swap').forEach(function(child) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(child, hasEventSource)
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement)
var source = internalData.sseEventSource
var sseSwapAttr = api.getAttributeValue(child, 'sse-swap')
var sseEventNames = sseSwapAttr.split(',')
for (var i = 0; i < sseEventNames.length; i++) {
var sseEventName = sseEventNames[i].trim()
var listener = function(event) {
// If the source is missing then close SSE
if (maybeCloseSSESource(sourceElement)) {
return
}
// If the body no longer contains the element, remove the listener
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener)
return
}
// swap the response into the DOM and trigger a notification
if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) {
return
}
swap(child, event.data)
api.triggerEvent(elt, 'htmx:sseMessage', event)
}
// Register the new listener
api.getInternalData(child).sseEventListener = listener
source.addEventListener(sseEventName, listener)
}
})
// Add message handlers for every `hx-trigger="sse:*"` attribute
queryAttributeOnThisOrChildren(elt, 'hx-trigger').forEach(function(child) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(child, hasEventSource)
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement)
var source = internalData.sseEventSource
var sseEventName = api.getAttributeValue(child, 'hx-trigger')
if (sseEventName == null) {
return
}
// Only process hx-triggers for events with the "sse:" prefix
if (sseEventName.slice(0, 4) != 'sse:') {
return
}
var listener = function(event) {
if (maybeCloseSSESource(sourceElement)) {
return
}
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener)
}
// Trigger events to be handled by the rest of htmx
htmx.trigger(child, sseEventName, event)
htmx.trigger(child, 'htmx:sseMessage', event)
}
// Register the new listener
api.getInternalData(elt).sseEventListener = listener
source.addEventListener(sseEventName.slice(4), listener)
})
}
/**
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
* is created and stored in the element's internalData.
* @param {HTMLElement} elt
* @param {number} retryCount
* @returns {EventSource | null}
*/
function ensureEventSourceOnElement(elt, retryCount) {
if (elt == null) {
return null
}
// handle extension source creation attribute
queryAttributeOnThisOrChildren(elt, 'sse-connect').forEach(function(child) {
var sseURL = api.getAttributeValue(child, 'sse-connect')
if (sseURL == null) {
return
}
ensureEventSource(child, sseURL, retryCount)
})
registerSSE(elt)
}
function ensureEventSource(elt, url, retryCount) {
var source = htmx.createEventSource(url)
source.onerror = function(err) {
// Log an error event
api.triggerErrorEvent(elt, 'htmx:sseError', { error: err, source })
// If parent no longer exists in the document, then clean up this EventSource
if (maybeCloseSSESource(elt)) {
return
}
// Otherwise, try to reconnect the EventSource
if (source.readyState === EventSource.CLOSED) {
retryCount = retryCount || 0
var timeout = Math.random() * (2 ^ retryCount) * 500
window.setTimeout(function() {
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1))
}, timeout)
}
}
source.onopen = function(evt) {
api.triggerEvent(elt, 'htmx:sseOpen', { source })
}
api.getInternalData(elt).sseEventSource = source
}
/**
* maybeCloseSSESource confirms that the parent element still exists.
* If not, then any associated SSE source is closed and the function returns true.
*
* @param {HTMLElement} elt
* @returns boolean
*/
function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) {
var source = api.getInternalData(elt).sseEventSource
if (source != undefined) {
source.close()
// source = null
return true
}
}
return false
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = []
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName)) {
result.push(elt)
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + ']').forEach(function(node) {
result.push(node)
})
return result
}
/**
* @param {HTMLElement} elt
* @param {string} content
*/
function swap(elt, content) {
api.withExtensions(elt, function(extension) {
content = extension.transformResponse(content, null, elt)
})
var swapSpec = api.getSwapSpecification(elt)
var target = api.getTarget(elt)
api.swap(target, content, swapSpec)
}
/**
* doSettle mirrors much of the functionality in htmx that
* settles elements after their content has been swapped.
* TODO: this should be published by htmx, and not duplicated here
* @param {import("../htmx").HtmxSettleInfo} settleInfo
* @returns () => void
*/
function doSettle(settleInfo) {
return function() {
settleInfo.tasks.forEach(function(task) {
task.call()
})
settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.remove(htmx.config.settlingClass)
}
api.triggerEvent(elt, 'htmx:afterSettle')
})
}
}
function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null
}
})()

467
site/htmx-assets/ext/ws.js Normal file
View file

@ -0,0 +1,467 @@
/*
WebSockets Extension
============================
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
*/
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api
htmx.defineExtension('ws', {
/**
* init is called once, when this extension is first registered.
* @param {import("../htmx").HtmxInternalApi} apiRef
*/
init: function(apiRef) {
// Store reference to internal API
api = apiRef
// Default function for creating new EventSource objects
if (!htmx.createWebSocket) {
htmx.createWebSocket = createWebSocket
}
// Default setting for reconnect delay
if (!htmx.config.wsReconnectDelay) {
htmx.config.wsReconnectDelay = 'full-jitter'
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
*/
onEvent: function(name, evt) {
var parent = evt.target || evt.detail.elt
switch (name) {
// Try to close the socket when elements are removed
case 'htmx:beforeCleanupElement':
var internalData = api.getInternalData(parent)
if (internalData.webSocket) {
internalData.webSocket.close()
}
return
// Try to create websockets when elements are processed
case 'htmx:beforeProcessNode':
forEach(queryAttributeOnThisOrChildren(parent, 'ws-connect'), function(child) {
ensureWebSocket(child)
})
forEach(queryAttributeOnThisOrChildren(parent, 'ws-send'), function(child) {
ensureWebSocketSend(child)
})
}
}
})
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/)
}
function getLegacyWebsocketURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, 'hx-ws')
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue)
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/)
if (value[0] === 'connect') {
return value[1]
}
}
}
}
/**
* ensureWebSocket creates a new WebSocket on the designated element, using
* the element's "ws-connect" attribute.
* @param {HTMLElement} socketElt
* @returns
*/
function ensureWebSocket(socketElt) {
// If the element containing the WebSocket connection no longer exists, then
// do not connect/reconnect the WebSocket.
if (!api.bodyContains(socketElt)) {
return
}
// Get the source straight from the element's value
var wssSource = api.getAttributeValue(socketElt, 'ws-connect')
if (wssSource == null || wssSource === '') {
var legacySource = getLegacyWebsocketURL(socketElt)
if (legacySource == null) {
return
} else {
wssSource = legacySource
}
}
// Guarantee that the wssSource value is a fully qualified URL
if (wssSource.indexOf('/') === 0) {
var base_part = location.hostname + (location.port ? ':' + location.port : '')
if (location.protocol === 'https:') {
wssSource = 'wss://' + base_part + wssSource
} else if (location.protocol === 'http:') {
wssSource = 'ws://' + base_part + wssSource
}
}
var socketWrapper = createWebsocketWrapper(socketElt, function() {
return htmx.createWebSocket(wssSource)
})
socketWrapper.addEventListener('message', function(event) {
if (maybeCloseWebSocketSource(socketElt)) {
return
}
var response = event.data
if (!api.triggerEvent(socketElt, 'htmx:wsBeforeMessage', {
message: response,
socketWrapper: socketWrapper.publicInterface
})) {
return
}
api.withExtensions(socketElt, function(extension) {
response = extension.transformResponse(response, null, socketElt)
})
var settleInfo = api.makeSettleInfo(socketElt)
var fragment = api.makeFragment(response)
if (fragment.children.length) {
var children = Array.from(fragment.children)
for (var i = 0; i < children.length; i++) {
api.oobSwap(api.getAttributeValue(children[i], 'hx-swap-oob') || 'true', children[i], settleInfo)
}
}
api.settleImmediately(settleInfo.tasks)
api.triggerEvent(socketElt, 'htmx:wsAfterMessage', { message: response, socketWrapper: socketWrapper.publicInterface })
})
// Put the WebSocket into the HTML Element's custom data.
api.getInternalData(socketElt).webSocket = socketWrapper
}
/**
* @typedef {Object} WebSocketWrapper
* @property {WebSocket} socket
* @property {Array<{message: string, sendElt: Element}>} messageQueue
* @property {number} retryCount
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
* @property {(message: string, sendElt: Element) => void} send
* @property {(event: string, handler: Function) => void} addEventListener
* @property {() => void} handleQueuedMessages
* @property {() => void} init
* @property {() => void} close
*/
/**
*
* @param socketElt
* @param socketFunc
* @returns {WebSocketWrapper}
*/
function createWebsocketWrapper(socketElt, socketFunc) {
var wrapper = {
socket: null,
messageQueue: [],
retryCount: 0,
/** @type {Object<string, Function[]>} */
events: {},
addEventListener: function(event, handler) {
if (this.socket) {
this.socket.addEventListener(event, handler)
}
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(handler)
},
sendImmediately: function(message, sendElt) {
if (!this.socket) {
api.triggerErrorEvent()
}
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message,
socketWrapper: this.publicInterface
})) {
this.socket.send(message)
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
message,
socketWrapper: this.publicInterface
})
}
},
send: function(message, sendElt) {
if (this.socket.readyState !== this.socket.OPEN) {
this.messageQueue.push({ message, sendElt })
} else {
this.sendImmediately(message, sendElt)
}
},
handleQueuedMessages: function() {
while (this.messageQueue.length > 0) {
var queuedItem = this.messageQueue[0]
if (this.socket.readyState === this.socket.OPEN) {
this.sendImmediately(queuedItem.message, queuedItem.sendElt)
this.messageQueue.shift()
} else {
break
}
}
},
init: function() {
if (this.socket && this.socket.readyState === this.socket.OPEN) {
// Close discarded socket
this.socket.close()
}
// Create a new WebSocket and event handlers
/** @type {WebSocket} */
var socket = socketFunc()
// The event.type detail is added for interface conformance with the
// other two lifecycle events (open and close) so a single handler method
// can handle them polymorphically, if required.
api.triggerEvent(socketElt, 'htmx:wsConnecting', { event: { type: 'connecting' } })
this.socket = socket
socket.onopen = function(e) {
wrapper.retryCount = 0
api.triggerEvent(socketElt, 'htmx:wsOpen', { event: e, socketWrapper: wrapper.publicInterface })
wrapper.handleQueuedMessages()
}
socket.onclose = function(e) {
// If socket should not be connected, stop further attempts to establish connection
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
var delay = getWebSocketReconnectDelay(wrapper.retryCount)
setTimeout(function() {
wrapper.retryCount += 1
wrapper.init()
}, delay)
}
// Notify client code that connection has been closed. Client code can inspect `event` field
// to determine whether closure has been valid or abnormal
api.triggerEvent(socketElt, 'htmx:wsClose', { event: e, socketWrapper: wrapper.publicInterface })
}
socket.onerror = function(e) {
api.triggerErrorEvent(socketElt, 'htmx:wsError', { error: e, socketWrapper: wrapper })
maybeCloseWebSocketSource(socketElt)
}
var events = this.events
Object.keys(events).forEach(function(k) {
events[k].forEach(function(e) {
socket.addEventListener(k, e)
})
})
},
close: function() {
this.socket.close()
}
}
wrapper.init()
wrapper.publicInterface = {
send: wrapper.send.bind(wrapper),
sendImmediately: wrapper.sendImmediately.bind(wrapper),
queue: wrapper.messageQueue
}
return wrapper
}
/**
* ensureWebSocketSend attaches trigger handles to elements with
* "ws-send" attribute
* @param {HTMLElement} elt
*/
function ensureWebSocketSend(elt) {
var legacyAttribute = api.getAttributeValue(elt, 'hx-ws')
if (legacyAttribute && legacyAttribute !== 'send') {
return
}
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
processWebSocketSend(webSocketParent, elt)
}
/**
* hasWebSocket function checks if a node has webSocket instance attached
* @param {HTMLElement} node
* @returns {boolean}
*/
function hasWebSocket(node) {
return api.getInternalData(node).webSocket != null
}
/**
* processWebSocketSend adds event listeners to the <form> element so that
* messages can be sent to the WebSocket server when the form is submitted.
* @param {HTMLElement} socketElt
* @param {HTMLElement} sendElt
*/
function processWebSocketSend(socketElt, sendElt) {
var nodeData = api.getInternalData(sendElt)
var triggerSpecs = api.getTriggerSpecs(sendElt)
triggerSpecs.forEach(function(ts) {
api.addTriggerHandler(sendElt, ts, nodeData, function(elt, evt) {
if (maybeCloseWebSocketSource(socketElt)) {
return
}
/** @type {WebSocketWrapper} */
var socketWrapper = api.getInternalData(socketElt).webSocket
var headers = api.getHeaders(sendElt, api.getTarget(sendElt))
var results = api.getInputValues(sendElt, 'post')
var errors = results.errors
var rawParameters = results.values
var expressionVars = api.getExpressionVars(sendElt)
var allParameters = api.mergeObjects(rawParameters, expressionVars)
var filteredParameters = api.filterValues(allParameters, sendElt)
var sendConfig = {
parameters: filteredParameters,
unfilteredParameters: allParameters,
headers,
errors,
triggeringEvent: evt,
messageBody: undefined,
socketWrapper: socketWrapper.publicInterface
}
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
return
}
if (errors && errors.length > 0) {
api.triggerEvent(elt, 'htmx:validation:halted', errors)
return
}
var body = sendConfig.messageBody
if (body === undefined) {
var toSend = Object.assign({}, sendConfig.parameters)
if (sendConfig.headers) { toSend.HEADERS = headers }
body = JSON.stringify(toSend)
}
socketWrapper.send(body, elt)
if (evt && api.shouldCancel(evt, elt)) {
evt.preventDefault()
}
})
})
}
/**
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
* @param {number} retryCount // The number of retries that have already taken place
* @returns {number}
*/
function getWebSocketReconnectDelay(retryCount) {
/** @type {"full-jitter" | ((retryCount:number) => number)} */
var delay = htmx.config.wsReconnectDelay
if (typeof delay === 'function') {
return delay(retryCount)
}
if (delay === 'full-jitter') {
var exp = Math.min(retryCount, 6)
var maxDelay = 1000 * Math.pow(2, exp)
return maxDelay * Math.random()
}
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')
}
/**
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
* returns FALSE.
*
* @param {*} elt
* @returns
*/
function maybeCloseWebSocketSource(elt) {
if (!api.bodyContains(elt)) {
api.getInternalData(elt).webSocket.close()
return true
}
return false
}
/**
* createWebSocket is the default method for creating new WebSocket objects.
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
*
* @param {string} url
* @returns WebSocket
*/
function createWebSocket(url) {
var sock = new WebSocket(url, [])
sock.binaryType = htmx.config.wsBinaryType
return sock
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = []
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, 'hx-ws')) {
result.push(elt)
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + '], [data-hx-ws], [hx-ws]').forEach(function(node) {
result.push(node)
})
return result
}
/**
* @template T
* @param {T[]} arr
* @param {(T) => void} func
*/
function forEach(arr, func) {
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i])
}
}
}
})()

1
site/htmx-assets/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,173 @@
<div class="min-h-screen ds-bg-page">
<section class="py-20 ds-container">
<div class="mx-auto max-w-4xl text-center">
<div class="mb-8">
<div class="flex items-center justify-center gap-8 mb-6">
<img src="/images/logos/site-logo-b.png" alt="Site logo" class="lg:h-20 h-15" />
<img src="/images/JesusPerez_f.jpg" alt="Jesús Pérez Profile Picture" class="lg:w-40 rounded-full w-32 object-cover border-2 ds-border" />
</div>
<h1 class="text-balance text-4xl font-bold tracking-tight ds-text sm:text-6xl">
{{ texts["about-page-title"] }}
</h1>
<p class="mt-ds-6 ds-body ds-text-secondary max-w-2xl mx-auto">
{{ texts["about-page-subtitle"] }}
</p>
</div>
</div>
</section>
<section class="py-16 ds-bg">
<div class="ds-container-lg ds-container">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<h2 class="text-3xl font-bold ds-text mb-6">
{{ texts["about-my-journey"] }}
</h2>
<div class="space-y-4 ds-text-secondary">
<p>{{ texts["about-journey-paragraph-1"] }}</p>
<p>{{ texts["about-journey-paragraph-2"] }}</p>
<p>{{ texts["about-journey-paragraph-3"] }}</p>
</div>
</div>
<div class="p-8 ds-rounded-lg">
<h3 class="ds-heading-4 ds-text mb-ds-4">
{{ texts["about-philosophy-approach"] }}
</h3>
<ul class="space-y-3 ds-text-secondary">
<li class="flex items-start gap-3">
<span class="text-green-500 mt-1">✓</span>
<span>{{ texts["about-open-source-first"] }}</span>
</li>
<li class="flex items-start gap-3">
<span class="text-green-500 mt-1">✓</span>
<span>{{ texts["about-self-hosted-solutions"] }}</span>
</li>
<li class="flex items-start gap-3">
<span class="text-green-500 mt-1">✓</span>
<span>{{ texts["about-performance-security-design"] }}</span>
</li>
<li class="flex items-start gap-3">
<span class="text-green-500 mt-1">✓</span>
<span>{{ texts["about-pragmatic-solutions"] }}</span>
</li>
<li class="flex items-start gap-3">
<span class="text-green-500 mt-1">✓</span>
<span>{{ texts["about-knowledge-transfer-empowerment"] }}</span>
</li>
</ul>
</div>
</div>
</div>
</section>
<section class="py-16 ds-bg-page">
<div class="ds-container-lg ds-container">
<div class="text-center mb-ds-12">
<h2 class="text-3xl font-bold ds-text">{{ texts["about-technical-expertise"] }}</h2>
<p class="mt-4 ds-text-secondary">{{ texts["about-technologies-work-with"] }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="ds-bg p-ds-6 ds-rounded-lg ds-shadow-sm">
<div class="text-center mb-ds-4">
<span class="text-4xl">🦀</span>
</div>
<h3 class="ds-heading-4 ds-text mb-3 text-center">{{ texts["about-rust-development-title"] }}</h3>
<ul class="space-y-ds-2 ds-caption ds-text-secondary">
<li>{{ texts["about-systems-programming-item"] }}</li>
<li>{{ texts["about-web-applications-item"] }}</li>
<li>{{ texts["about-cli-tools-automation"] }}</li>
<li>{{ texts["about-performance-optimization-item"] }}</li>
<li>{{ texts["about-memory-safety-concurrency"] }}</li>
</ul>
</div>
<div class="ds-bg p-ds-6 ds-rounded-lg ds-shadow-sm">
<div class="text-center mb-ds-4">
<span class="text-4xl">🏗️</span>
</div>
<h3 class="ds-heading-4 ds-text mb-3 text-center">{{ texts["about-infrastructure-title"] }}</h3>
<ul class="space-y-ds-2 ds-caption ds-text-secondary">
<li>{{ texts["about-kubernetes-container"] }}</li>
<li>{{ texts["about-cicd-pipeline-design"] }}</li>
<li>{{ texts["about-monitoring-observability"] }}</li>
<li>{{ texts["about-self-hosted-alternatives"] }}</li>
<li>{{ texts["about-security-compliance"] }}</li>
</ul>
</div>
<div class="ds-bg p-ds-6 ds-rounded-lg ds-shadow-sm">
<div class="text-center mb-ds-4">
<span class="text-4xl">🌐</span>
</div>
<h3 class="ds-heading-4 ds-text mb-3 text-center">{{ texts["about-web3-blockchain-title"] }}</h3>
<ul class="space-y-ds-2 ds-caption ds-text-secondary">
<li>{{ texts["about-polkadot-ecosystem-dev"] }}</li>
<li>{{ texts["about-substrate-blockchain"] }}</li>
<li>{{ texts["about-smart-contract-dev"] }}</li>
<li>{{ texts["about-dapp-architecture"] }}</li>
<li>{{ texts["about-cross-chain-communication"] }}</li>
</ul>
</div>
</div>
</div>
</section>
<section class="py-16 ds-bg">
<div class="ds-container-lg ds-container">
<div class="text-center mb-ds-12">
<h2 class="text-3xl font-bold ds-text">{{ texts["about-why-self-hosted-open-source"] }}</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-ds-8">
<div class="space-y-6">
<div>
<h3 class="ds-body font-semibold ds-text mb-2">{{ texts["about-control-privacy"] }}</h3>
<p class="ds-text-secondary">{{ texts["about-control-privacy-desc"] }}</p>
</div>
<div>
<h3 class="ds-body font-semibold ds-text mb-2">{{ texts["about-cost-effectiveness"] }}</h3>
<p class="ds-text-secondary">{{ texts["about-cost-effectiveness-desc"] }}</p>
</div>
<div>
<h3 class="ds-body font-semibold ds-text mb-2">{{ texts["about-resilience"] }}</h3>
<p class="ds-text-secondary">{{ texts["about-resilience-desc"] }}</p>
</div>
</div>
<div class="space-y-6">
<div>
<h3 class="ds-body font-semibold ds-text mb-2">{{ texts["about-performance"] }}</h3>
<p class="ds-text-secondary">{{ texts["about-performance-desc"] }}</p>
</div>
<div>
<h3 class="ds-body font-semibold ds-text mb-2">{{ texts["about-customization"] }}</h3>
<p class="ds-text-secondary">{{ texts["about-customization-desc"] }}</p>
</div>
<div>
<h3 class="ds-body font-semibold ds-text mb-2">{{ texts["about-community"] }}</h3>
<p class="ds-text-secondary">{{ texts["about-community-desc"] }}</p>
</div>
</div>
</div>
</div>
</section>
<section class="py-16">
<div class="mx-auto max-w-4xl ds-container text-center">
<h2 class="text-3xl font-bold ds-text mb-ds-4">{{ texts["about-lets-work-together"] }}</h2>
<p class="ds-text-secondary mb-8 ds-body">{{ texts["about-work-together-desc"] }}</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ texts['about-contact-url'] | default('/contact') }}" class="no-underline ds-btn-secondary">
{{ texts["about-get-in-touch"] }}
</a>
<a href="{{ texts['about-work-request-url'] | default('/work-request') }}" class="no-underline ds-btn-secondary">
{{ texts["about-start-project"] }}
</a>
</div>
</div>
</section>
</div>

View file

@ -0,0 +1,116 @@
{# Level-aware project About — rendered when about.json declares kind="project".
Context: `about` (view-model), `lang`, `texts`.
Uses the ontoref `.doc-*` document chrome (styles/custom.css) so the SSR site
matches outreach/web. GENERIC over a block vocabulary: a section is
{ key, title, blocks }, each block a typed primitive (prose | timeline |
cards | links | graph | kv). New SECTIONS of an existing block type are
projector-only changes (gen-about-pages.nu); only a brand-new block TYPE
touches this template. Path/href values are `| safe` (trusted contract
values, not user input). Bilingual fields indexed by `lang`. #}
<div class="about-doc">
<div class="doc-wrap">
<header class="doc-head">
<p class="doc-eyebrow">{{ about.level_id }}</p>
<h1>{% if lang == "es" %}Acerca de{% else %}About{% endif %} {{ about.level_id }}</h1>
{% if about.hero and about.hero[lang] %}
<p class="doc-lead">{{ about.hero[lang] }}</p>
{% endif %}
</header>
{% set about_img = "ontoref-about-es" if lang == "es" else "ontoref-about-en" %}
<picture>
<source srcset="/images/{{ about_img }}.avif" type="image/avif">
<source srcset="/images/{{ about_img }}.webp" type="image/webp">
<img src="/images/{{ about_img }}.webp"
alt="{% if lang == 'es' %}Acerca de{% else %}About{% endif %} {{ about.level_id }}"
width="1536" height="1024" loading="lazy"
style="display:block;margin:1.5rem auto 0;max-width:48rem;width:100%;height:auto;border-radius:.5rem" />
</picture>
{% if about.sections | length > 1 %}
<nav class="doc-toc">
<p>{% if lang == "es" %}Secciones{% else %}Sections{% endif %}</p>
<ul>
{% for section in about.sections %}
<li><a href="#{{ section.key }}">{{ section.title[lang] }}</a></li>
{% endfor %}
</ul>
</nav>
{% endif %}
{% for section in about.sections %}
<section class="doc-section" id="{{ section.key }}">
<h2>{{ section.title[lang] }}</h2>
{% for block in section.blocks %}
{% if block.type == "prose" %}
<div class="doc-prose">{{ (block.html_es if lang == "es" and block.html_es else block.html) | safe }}</div>
{% elif block.type == "timeline" %}
{% if block.started %}
<p class="doc-mono" style="margin:0 0 .75rem">{% if lang == "es" %}Desde{% else %}Started{% endif %} {{ block.started }}</p>
{% endif %}
<ul class="doc-list">
{% for m in block.items %}
<li>
<span class="doc-mono">{{ m.date }}</span>
<span>{{ m.text[lang] }}</span>
</li>
{% endfor %}
</ul>
{% elif block.type == "cards" %}
{% for grp in block.groups %}
{% if grp.label %}<h3 class="doc-idx-h">{{ grp.label }} <span class="doc-count">({{ grp.items | length }})</span></h3>{% endif %}
<ul class="doc-cards">
{% for n in grp.items %}
{% set card_body = n.body_es if lang == "es" and n.body_es else n.body %}
<li>
<p class="doc-card-title">{{ n.title_es if lang == "es" and n.title_es else n.title }}{% if n.meta %} <span class="doc-card-meta">· {{ n.meta }}</span>{% endif %}</p>
{% if card_body %}<p class="doc-card-body">{{ card_body }}</p>{% endif %}
</li>
{% endfor %}
</ul>
{% endfor %}
{% elif block.type == "links" %}
{% for grp in block.groups %}
{% if grp.label %}<h3 class="doc-idx-h">{{ grp.label }} <span class="doc-count">({{ grp.items | length }})</span></h3>{% endif %}
<ul class="doc-idx-list">
{% for i in grp.items %}
<li>
<a href="{{ i.href | safe }}">{% if i.code %}<span class="doc-mono">{{ i.code }}</span> {% endif %}{{ i.text }}</a>
</li>
{% endfor %}
</ul>
{% endfor %}
{% if block.browse and block.browse.href %}
<a href="{{ block.browse.href | safe }}" class="doc-btn">{{ block.browse.label[lang] }} →</a>
{% endif %}
{% elif block.type == "graph" %}
{% if block.svg %}<div class="doc-graph">{{ block.svg | safe }}</div>{% endif %}
{% if block.live and block.live.href %}
<a href="{{ (block.live.href_es if lang == "es" and block.live.href_es else block.live.href) | safe }}" class="doc-btn">↗ {{ block.live.label[lang] }}</a>
{% endif %}
{% elif block.type == "kv" %}
<table class="doc-kv"><tbody>
{% for r in block.rows %}
<tr><td>{{ r.k_es if lang == "es" and r.k_es else r.k }}</td><td>{{ r.v_es if lang == "es" and r.v_es else r.v }}</td></tr>
{% endfor %}
</tbody></table>
{% endif %}
{% endfor %}
</section>
{% endfor %}
</div>
</div>
{% include "partials/image-zoom.j2" %}

View file

@ -0,0 +1,241 @@
<div class="ds-bg-page py-ds-6">
<section class="ds-container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{# ── Header ── #}
<div class="text-center mb-ds-8">
<h1 class="ds-heading-1 ds-text mb-ds-4">{{ texts["contact-page-title"] }}</h1>
<p class="mt-ds-6 ds-body ds-text-secondary max-w-2xl mx-auto">{{ texts["contact-page-subtitle"] | safe }}</p>
{% set contact_img = "ontoref-contactar" if lang == "es" else "ontoref-contact-en" %}
<picture>
<source srcset="/images/{{ contact_img }}.avif" type="image/avif">
<source srcset="/images/{{ contact_img }}.webp" type="image/webp">
<img src="/images/{{ contact_img }}.webp"
alt="{{ texts['contact-page-title'] }}"
width="1536" height="1024" loading="lazy"
class="mt-ds-8 mx-auto rounded-lg ds-shadow-sm w-full max-w-3xl h-auto" />
</picture>
</div>
{# ── Main grid: sidebar (1) + form (2) ── #}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-ds-6">
{# ── Contact info sidebar ── #}
<div class="lg:col-span-1">
<div class="ds-bg ds-rounded-lg ds-shadow-sm p-ds-6">
<h2 class="ds-heading-2 ds-text mb-ds-6">{{ texts["contact-lets-connect"] }}</h2>
<div class="space-y-6">
{# Email #}
<div class="flex items-start">
<div class="flex-shrink-0">
<div class="w-10 h-10 ds-bg-secondary ds-rounded-lg flex items-center justify-center">
<span class="text-lg">📧</span>
</div>
</div>
<div class="ml-4">
<h3 class="ds-heading-4 ds-text">{{ texts["contact-email-contact"] }}</h3>
<p class="ds-text-secondary">hello@jesusperez.pro</p>
<p class="ds-caption ds-text-muted">{{ texts["contact-best-for-detailed"] }}</p>
</div>
</div>
{# Telegram #}
<div class="flex items-start">
<div class="flex-shrink-0">
<div class="w-10 h-10 ds-bg-secondary ds-rounded-lg flex items-center justify-center">
<span class="text-lg">💬</span>
</div>
</div>
<div class="ml-4">
<h3 class="ds-heading-4 ds-text">{{ texts["contact-telegram-contact"] }}</h3>
<p class="ds-text-secondary">@jesusperez_dev</p>
<p class="ds-caption ds-text-muted">{{ texts["contact-quick-questions"] }}</p>
</div>
</div>
{# Schedule a call #}
<div class="flex items-start">
<div class="flex-shrink-0">
<div class="w-10 h-10 ds-bg-secondary ds-rounded-lg flex items-center justify-center">
<span class="text-lg">📞</span>
</div>
</div>
<div class="ml-4">
<h3 class="ds-heading-4 ds-text">{{ texts["contact-schedule-call"] }}</h3>
<p class="ds-text-secondary">{{ texts["contact-consultation-30min"] }}</p>
<a href="https://calendly.com/jesusperez-dev" target="_blank" rel="noopener noreferrer"
class="ds-caption ds-text font-medium hover:ds-text-secondary">
{{ texts["contact-book-time-slot"] }}
</a>
</div>
</div>
</div>
{# Professional networks #}
<div class="mt-ds-6 pt-ds-6 border-t ds-border">
<h3 class="ds-heading-4 ds-text mb-ds-4">{{ texts["contact-professional-networks"] }}</h3>
<div class="space-y-2">
<a href="https://www.linkedin.com/in/jesusperezlorenzo/" target="_blank" rel="noopener noreferrer"
class="no-underline block ds-text-secondary hover:ds-text ds-caption">
{{ texts["contact-linkedin-profile"] }}
</a>
<a href="https://repo.jesusperez.pro/jesus/ontoref" target="_blank" rel="noopener noreferrer"
class="no-underline block ds-text-secondary hover:ds-text ds-caption">
{{ texts["contact-git-profile"] }}
</a>
<a href="{{ texts['contact-blog-url'] | default(value='/blog') }}"
class="no-underline block ds-text-secondary hover:ds-text ds-caption">
{{ texts["contact-read-my-blog"] }}
</a>
</div>
</div>
</div>
</div>
{# ── Contact form (2 cols) ── #}
<div class="lg:col-span-2">
<div class="ds-bg ds-rounded-lg ds-shadow-sm p-ds-6">
<div class="form-header mb-ds-6">
<h2 class="ds-heading-2 ds-text mb-2">{{ texts["contact-send-message"] }}</h2>
<p class="ds-text-secondary">{{ texts["contact-contact-form-description"] }}</p>
</div>
<form class="space-y-ds-4"
hx-post="/api/contact"
hx-target="#contact-form-result"
hx-swap="innerHTML">
{# Name + Email row #}
<div class="grid grid-cols-1 md:grid-cols-2 gap-ds-4">
<div>
<label for="contact-form-name" class="ds-text font-medium block mb-2">
{{ texts["contact-name-label"] }}
</label>
<input id="contact-form-name" name="name" type="text" required
class="ds-input w-full p-3 ds-border ds-rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="{{ texts['contact-name-placeholder'] }}" />
</div>
<div>
<label for="contact-form-email" class="ds-text font-medium block mb-2">
{{ texts["contact-email-label"] }}
</label>
<input id="contact-form-email" name="email" type="email" required
class="ds-input w-full p-3 ds-border ds-rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="{{ texts['contact-email-placeholder'] }}" />
</div>
</div>
{# Subject #}
<div>
<label for="contact-form-subject" class="ds-text font-medium block mb-2">
{{ texts["contact-subject-label"] }}
</label>
<input id="contact-form-subject" name="subject" type="text" required
class="ds-input w-full p-3 ds-border ds-rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="{{ texts['contact-subject-placeholder'] }}" />
</div>
{# Message #}
<div>
<label for="contact-form-message" class="ds-text font-medium block mb-2">
{{ texts["contact-message-label"] }}
</label>
<textarea id="contact-form-message" name="message" rows="6" required
class="ds-textarea w-full p-3 ds-border ds-rounded focus:outline-none focus:ring-2 focus:ring-blue-500 resize-vertical"
placeholder="{{ texts['contact-message-placeholder'] }}"></textarea>
</div>
<div class="flex justify-end">
<button type="submit"
class="ds-btn-primary px-8 py-3 font-medium ds-rounded transition-colors duration-200">
{{ texts["contact-send-message-btn"] }}
</button>
</div>
<div id="contact-form-result"></div>
</form>
</div>
{# Response time box #}
<div class="mt-ds-6 p-ds-4 ds-bg-secondary ds-rounded-lg">
<h3 class="font-semibold ds-text mb-2">{{ texts["contact-response-time"] }}</h3>
<p class="ds-caption ds-text-secondary">{{ texts["contact-response-time-text"] }}</p>
</div>
</div>
</div>
{# ── FAQ section ── #}
<div class="mt-ds-8">
<div class="ds-bg ds-rounded-lg ds-shadow-sm p-ds-6">
<h2 class="ds-heading-2 ds-text mb-ds-6 text-center">{{ texts["contact-frequently-asked-questions"] }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-ds-6">
<div>
<h3 class="ds-heading-4 ds-text mb-2">{{ texts["contact-faq-what-projects"] }}</h3>
<p class="ds-text-secondary">{{ texts["contact-faq-what-projects-answer"] }}</p>
</div>
<div>
<h3 class="ds-heading-4 ds-text mb-2">{{ texts["contact-faq-remote-teams"] }}</h3>
<p class="ds-text-secondary">{{ texts["contact-faq-remote-teams-answer"] }}</p>
</div>
<div>
<h3 class="ds-heading-4 ds-text mb-2">{{ texts["contact-faq-pricing-approach"] }}</h3>
<p class="ds-text-secondary">{{ texts["contact-faq-pricing-answer"] }}</p>
</div>
<div>
<h3 class="ds-heading-4 ds-text mb-2">{{ texts["contact-faq-existing-projects"] }}</h3>
<p class="ds-text-secondary">{{ texts["contact-faq-existing-projects-answer"] }}</p>
</div>
</div>
</div>
</div>
{# ── Other ways to connect ── #}
<div class="mt-ds-8">
<div class="text-center">
<h2 class="ds-heading-2 ds-text mb-ds-6">{{ texts["contact-other-ways-connect"] }}</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-ds-6">
<div class="ds-bg-secondary ds-rounded-lg p-ds-6">
<div class="ds-text-secondary mb-ds-4">
<span class="text-4xl">🚀</span>
</div>
<h3 class="ds-heading-4 ds-text mb-2">{{ texts["contact-project-consultation"] }}</h3>
<p class="ds-text-secondary mb-ds-4">{{ texts["contact-consultation-help"] }}</p>
<a href="{{ texts['contact-services-url'] | default(value='/services') }}#tier2"
class="no-underline ds-btn-primary">
{{ texts["contact-request-quote"] }}
</a>
</div>
<div class="ds-bg-secondary ds-rounded-lg p-ds-6">
<div class="ds-text-secondary mb-ds-4">
<span class="text-4xl">🧑‍🏫</span>
</div>
<h3 class="ds-heading-4 ds-text mb-2">{{ texts["contact-mentoring-training"] }}</h3>
<p class="ds-text-secondary mb-ds-4">{{ texts["contact-mentoring-help"] }}</p>
<a href="{{ texts['contact-services-url'] | default(value='/services') }}#learn"
class="ds-btn-primary">
{{ texts["contact-learn-more"] }}
</a>
</div>
<div class="ds-bg-secondary ds-rounded-lg p-ds-6">
<div class="ds-text-secondary mb-ds-4">
<span class="text-4xl">🎤</span>
</div>
<h3 class="ds-heading-4 ds-text mb-2">{{ texts["contact-speaking-events"] }}</h3>
<p class="ds-text-secondary mb-ds-4">{{ texts["contact-speaking-help"] }}</p>
<a href="mailto:hello@jesusperez.pro?subject=Speaking Opportunity"
class="ds-btn-primary">
{{ texts["contact-get-in-touch"] }}
</a>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partials/image-zoom.j2" %}

View file

@ -0,0 +1,220 @@
<div class="ds-bg-page">
{# TEMP: hide the Posts/Recipes door CTAs for now (remove this <style> to restore) #}
<style>.tagline-panel-graph{display:none}</style>
{# Hero — three-door vestibule, projected from .ontoref/positioning/spine.ncl (ADR-057).
Never an exposition of the core: the doors route, the prize is revealed once inside.
Tech features live below the fold (Three Pillars / How it works). #}
<section class="relative py-ds-8 ds-container">
<div class="mx-auto max-w-4xl text-center">
<span class="inline-block px-3 py-1 ds-caption ds-bg border ds-border ds-rounded-md ds-text-secondary mb-4 font-mono">
{{ texts["home-hero-badge"] }}
</span>
{# A · hero — the language-specific banner carries logo + headline + sub-hero + the
four value nodes (baked in) and scales fluidly at every width, so it shows at all
breakpoints. The h1/sub stay sr-only for SEO + a11y; the <picture> is click-to-zoom. #}
<h1 class="sr-only">{{ texts["spine-hero"] }}</h1>
<p class="sr-only">{{ texts["spine-sub"] }}</p>
{% set hero_img = "ontoref-home-seguro" if lang == "es" else "ontoref-home-sure-footed" %}
<picture>
<source srcset="/images/{{ hero_img }}.avif" type="image/avif">
<source srcset="/images/{{ hero_img }}.webp" type="image/webp">
<img src="/images/{{ hero_img }}.webp"
alt="{{ texts['spine-hero'] }}"
width="1220" height="417" loading="eager"
class="block mx-auto rounded-lg w-full max-w-4xl h-auto mb-6" />
</picture>
{# rotating definition-phrases by audience + focus #}
<p class="ds-body-lg ds-text mb-1" style="min-height: 1.6em">
<span
data-tagline-rotator
data-audience="all"
data-src="/public/r/taglines.json"
data-focus-target=".tagline-focus"
>{{ texts["spine-hero"] }}</span
>
</p>
<span class="tagline-focus" aria-hidden="true"></span>
{# C · four doors — primary nav: each switches taglines AND reveals its panel #}
<div class="tagline-doors" role="group" aria-label="{{ texts['spine-doors-heading'] }}">
<button type="button" class="tagline-door is-active" data-tagline-audience="all" data-door="all">{{ texts["home-tagline-door-all"] }}</button>
<button type="button" class="tagline-door" data-tagline-audience="arriving-developer" data-door="developer">{{ texts["spine-door-developer-label"] }}</button>
<button type="button" class="tagline-door" data-tagline-audience="infrastructure-inheritor" data-door="infrastructure">{{ texts["spine-door-infrastructure-label"] }}</button>
<button type="button" class="tagline-door" data-tagline-audience="personal-ontology" data-door="personal">{{ texts["spine-door-personal-label"] }}</button>
<button type="button" class="tagline-door" data-tagline-audience="authoring-knowledge" data-door="authoring">{{ texts["spine-door-authoring-label"] }}</button>
</div>
{# door panels — hidden until a door is clicked; 'all' hides all. A teaser that
ROUTES to the door landing (full value-prop) + the live graph, never argues. #}
<div class="tagline-panels max-w-2xl mx-auto">
<div class="tagline-panel" data-tagline-panel="developer" hidden>
<p class="tagline-panel-line">{{ texts["spine-door-developer-line"] }}</p>
<div class="tagline-panel-cta">
<a class="no-underline ds-btn-secondary" href="{{ texts['spine-door-developer-href'] }}">{{ texts["spine-door-cta"] }}</a>
<a class="no-underline tagline-panel-graph" href="{{ texts['spine-door-developer-blog'] }}">{{ texts["spine-door-blog-cta"] }}</a>
<a class="no-underline tagline-panel-graph" href="{{ texts['spine-door-developer-recipes'] }}">{{ texts["spine-door-recipes-cta"] }}</a>
</div>
</div>
<div class="tagline-panel" data-tagline-panel="infrastructure" hidden>
<p class="tagline-panel-line">{{ texts["spine-door-infrastructure-line"] }}</p>
<div class="tagline-panel-cta">
<a class="no-underline ds-btn-secondary" href="{{ texts['spine-door-infrastructure-href'] }}">{{ texts["spine-door-cta"] }}</a>
<a class="no-underline tagline-panel-graph" href="{{ texts['spine-door-infrastructure-blog'] }}">{{ texts["spine-door-blog-cta"] }}</a>
<a class="no-underline tagline-panel-graph" href="{{ texts['spine-door-infrastructure-recipes'] }}">{{ texts["spine-door-recipes-cta"] }}</a>
</div>
</div>
<div class="tagline-panel" data-tagline-panel="personal" hidden>
<p class="tagline-panel-line">{{ texts["spine-door-personal-line"] }}</p>
<div class="tagline-panel-cta">
<a class="no-underline ds-btn-secondary" href="{{ texts['spine-door-personal-href'] }}">{{ texts["spine-door-cta"] }}</a>
<a class="no-underline tagline-panel-graph" href="{{ texts['spine-door-personal-blog'] }}">{{ texts["spine-door-blog-cta"] }}</a>
<a class="no-underline tagline-panel-graph" href="{{ texts['spine-door-personal-recipes'] }}">{{ texts["spine-door-recipes-cta"] }}</a>
</div>
</div>
<div class="tagline-panel" data-tagline-panel="authoring" hidden>
<p class="tagline-panel-line">{{ texts["spine-door-authoring-line"] }}</p>
<div class="tagline-panel-cta">
<a class="no-underline ds-btn-secondary" href="{{ texts['spine-door-authoring-href'] }}">{{ texts["spine-door-cta"] }}</a>
<a class="no-underline tagline-panel-graph" href="{{ texts['spine-door-authoring-blog'] }}">{{ texts["spine-door-blog-cta"] }}</a>
<a class="no-underline tagline-panel-graph" href="{{ texts['spine-door-authoring-recipes'] }}">{{ texts["spine-door-recipes-cta"] }}</a>
</div>
</div>
</div>
{# D · prize — the reward once inside #}
<p class="ds-caption font-mono mt-8 mb-6 max-w-2xl mx-auto" style="color: #f97316">
{{ texts["spine-prize"] }}
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="/blog" class="no-underline ds-btn-primary">
{{ texts["home-cta-explore"] }}
</a>
<a href="https://github.com/stratum-iops/ontoref" target="_blank" rel="noopener noreferrer"
class="no-underline ds-btn-secondary inline-flex items-center gap-2">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 10 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0 0 20 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd"/>
</svg>
{{ texts["home-cta-github"] }}
</a>
</div>
{# E · close — bookend with the hero #}
<p class="ds-caption ds-text-secondary mt-8 italic">{{ texts["spine-close"] }}</p>
</div>
</section>
{# Three Pillars #}
<section class="py-ds-8 ds-bg ds-rounded-lg">
<div class="ds-container-lg ds-container">
<div class="text-center mb-ds-8">
<h2 class="text-3xl font-bold ds-text">{{ texts["home-pillars-title"] }}</h2>
<p class="mt-3 ds-text-secondary ds-body">{{ texts["home-pillars-subtitle"] }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm border ds-border">
<div class="mb-4">
<span class="ds-caption font-mono px-2 py-1 ds-bg border ds-border ds-rounded ds-text-secondary">
{{ texts["home-pillar-impact-label"] }}
</span>
</div>
<h3 class="ds-heading-4 ds-text mb-3">{{ texts["home-pillar-impact-title"] }}</h3>
<p class="ds-text-secondary ds-body-sm">{{ texts["home-pillar-impact-desc"] }}</p>
</div>
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm border ds-border">
<div class="mb-4">
<span class="ds-caption font-mono px-2 py-1 ds-bg border ds-border ds-rounded ds-text-secondary">
{{ texts["home-pillar-witness-label"] }}
</span>
</div>
<h3 class="ds-heading-4 ds-text mb-3">{{ texts["home-pillar-witness-title"] }}</h3>
<p class="ds-text-secondary ds-body-sm">{{ texts["home-pillar-witness-desc"] }}</p>
</div>
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm border ds-border">
<div class="mb-4">
<span class="ds-caption font-mono px-2 py-1 ds-bg border ds-border ds-rounded ds-text-secondary">
{{ texts["home-pillar-shared-label"] }}
</span>
</div>
<h3 class="ds-heading-4 ds-text mb-3">{{ texts["home-pillar-shared-title"] }}</h3>
<p class="ds-text-secondary ds-body-sm">{{ texts["home-pillar-shared-desc"] }}</p>
</div>
</div>
</div>
</section>
{# Architecture image — config-driven & optional: shown only when the consumer's
home FTL declares `home-arch-img-light` (per-site content, never hardcoded). #}
{% if texts["home-arch-img-light"] %}
<section class="py-ds-8 ds-container">
<div class="mx-auto max-w-4xl">
<h2 class="text-2xl font-bold ds-text mb-6 text-center">{{ texts["home-arch-title"] }}</h2>
{# The SVG carries its own dark surface + rounded corners — no wrapper card, or
its background double-frames and clashes. Shown in both themes; wrapped in
<picture> so the global lightbox makes it click-to-zoom. #}
<div class="text-center">
<picture>
<img src="{{ texts['home-arch-img-light'] }}"
alt="{{ texts['home-arch-title'] }}"
class="block mx-auto w-full h-auto rounded-lg" />
</picture>
<p class="ds-caption ds-text-secondary mt-3">{{ texts["home-arch-caption"] }}</p>
</div>
</div>
</section>
{% endif %}
{# How it works #}
<section class="py-ds-8 ds-bg ds-rounded-lg">
<div class="ds-container-lg ds-container">
<div class="text-center mb-ds-8">
<h2 class="text-3xl font-bold ds-text">{{ texts["home-how-title"] }}</h2>
<p class="mt-3 ds-text-secondary ds-body">{{ texts["home-how-subtitle"] }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm">
<div class="w-8 h-8 ds-rounded-full flex items-center justify-center font-bold ds-text mb-4"
style="background: #3b82f620; color: #60a5fa; border: 1px solid #3b82f640">1</div>
<h3 class="font-mono ds-text font-semibold mb-2">{{ texts["home-how-step1-title"] }}</h3>
<p class="ds-text-secondary ds-body-sm">{{ texts["home-how-step1-desc"] }}</p>
</div>
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm">
<div class="w-8 h-8 ds-rounded-full flex items-center justify-center font-bold ds-text mb-4"
style="background: #3b82f620; color: #60a5fa; border: 1px solid #3b82f640">2</div>
<h3 class="font-mono ds-text font-semibold mb-2">{{ texts["home-how-step2-title"] }}</h3>
<p class="ds-text-secondary ds-body-sm">{{ texts["home-how-step2-desc"] }}</p>
</div>
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm">
<div class="w-8 h-8 ds-rounded-full flex items-center justify-center font-bold ds-text mb-4"
style="background: #3b82f620; color: #60a5fa; border: 1px solid #3b82f640">3</div>
<h3 class="font-mono ds-text font-semibold mb-2">{{ texts["home-how-step3-title"] }}</h3>
<p class="ds-text-secondary ds-body-sm">{{ texts["home-how-step3-desc"] }}</p>
</div>
</div>
</div>
</section>
{# CTA #}
<section class="py-ds-10 ds-container">
<div class="mx-auto max-w-3xl text-center">
<h2 class="text-3xl font-bold ds-text mb-4">{{ texts["home-cta-title"] }}</h2>
<p class="ds-body ds-text-secondary mb-8">{{ texts["home-cta-desc"] }}</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="/blog" class="no-underline ds-btn-primary">
{{ texts["home-cta-primary"] }}
</a>
<a href="/blog" class="no-underline ds-btn-secondary">
{{ texts["home-cta-secondary"] }}
</a>
</div>
</div>
</section>
{# Hero tagline rotator — typed.js (typewriter) + native fade fallback #}
<script src="/js/typed.umd.js"></script>
<script src="/js/tagline-rotator.js"></script>
</div>

View file

@ -0,0 +1,22 @@
{# 404 Not Found page body. Context: path, lang #}
<div class="flex flex-col items-center justify-center min-h-64 px-4 py-16 text-center">
<p class="text-8xl font-bold ds-text-muted mb-4" aria-hidden="true">404</p>
<h1 class="text-2xl font-semibold ds-text mb-2">
{% if lang == "es" %}Página no encontrada{% else %}Page not found{% endif %}
</h1>
<p class="ds-text-muted mb-8">
{% if lang == "es" %}
La ruta <code class="font-mono text-sm">{{ path }}</code> no existe.
{% else %}
The path <code class="font-mono text-sm">{{ path }}</code> does not exist.
{% endif %}
</p>
<a href="/"
class="ds-btn-ghost-sm no-underline"
hx-get="/"
hx-target="#main-content"
hx-swap="innerHTML"
hx-push-url="true">
{% if lang == "es" %}← Volver al inicio{% else %}← Back to home{% endif %}
</a>
</div>

View file

@ -0,0 +1,96 @@
{# Content post viewer — website override of the framework post.j2.
Differences from the framework default:
- No max-w-3xl / mx-auto on article (column width already constrained by grid).
- post-content-body on the prose div so the lead-image CSS rule applies.
- overflow-hidden on article to prevent images from bleeding into the sidebar. #}
<article class="w-full overflow-hidden px-4 py-8" itemscope itemtype="https://schema.org/Article">
{% include "partials/content_header.j2" %}
{% if item.excerpt %}
<div class="text-lg ds-text-muted leading-relaxed mb-8 border-l-4 border-current pl-4 italic">
{{ item.excerpt }}
</div>
{% endif %}
<div class="prose prose-neutral dark:prose-invert max-w-none ds-text leading-relaxed post-content-body" itemprop="articleBody">
{{ body_html | safe }}
</div>
{# ── Per-post engagement: rating · comment · views ─────────────────────────
Endpoints live in the rustelo_server `post-engagement` feature. Layout is
styled in custom.css (pe-*), not Tailwind utilities, so spacing is reliable. #}
<section class="pe-engagement" aria-label="{% if lang == 'es' %}Interacción{% else %}Engagement{% endif %}">
{# Rating CTA — invites a vote; loads current average + your own vote on render. #}
<div class="pe-block pe-rate-block">
<span class="pe-cta-label">
{% if lang == "es" %}¿Te ha resultado útil? Valóralo{% else %}Was this useful? Rate it{% endif %}
</span>
<div id="post-rating" class="pe-rating"
hx-get="/api/htmx/post/{{ item.id }}/rating"
hx-trigger="load"
hx-vals='{"lang":"{{ lang }}"}'
hx-swap="outerHTML">
<div class="pe-stars">
<button type="button" class="pe-star" aria-label="{% if lang == 'es' %}Valorar{% else %}Rate{% endif %} 1" hx-post="/api/htmx/post/{{ item.id }}/rate" hx-vals='{"value":1,"lang":"{{ lang }}"}' hx-target="#post-rating" hx-swap="outerHTML">★</button>
<button type="button" class="pe-star" aria-label="{% if lang == 'es' %}Valorar{% else %}Rate{% endif %} 2" hx-post="/api/htmx/post/{{ item.id }}/rate" hx-vals='{"value":2,"lang":"{{ lang }}"}' hx-target="#post-rating" hx-swap="outerHTML">★</button>
<button type="button" class="pe-star" aria-label="{% if lang == 'es' %}Valorar{% else %}Rate{% endif %} 3" hx-post="/api/htmx/post/{{ item.id }}/rate" hx-vals='{"value":3,"lang":"{{ lang }}"}' hx-target="#post-rating" hx-swap="outerHTML">★</button>
<button type="button" class="pe-star" aria-label="{% if lang == 'es' %}Valorar{% else %}Rate{% endif %} 4" hx-post="/api/htmx/post/{{ item.id }}/rate" hx-vals='{"value":4,"lang":"{{ lang }}"}' hx-target="#post-rating" hx-swap="outerHTML">★</button>
<button type="button" class="pe-star" aria-label="{% if lang == 'es' %}Valorar{% else %}Rate{% endif %} 5" hx-post="/api/htmx/post/{{ item.id }}/rate" hx-vals='{"value":5,"lang":"{{ lang }}"}' hx-target="#post-rating" hx-swap="outerHTML">★</button>
</div>
</div>
</div>
{# Comment CTA — invites participation; opens the modal form in #modal-slot. #}
<div class="pe-block pe-msg-block">
<div class="pe-msg-copy">
<span class="pe-cta-label">
{% if lang == "es" %}¿Tienes algo que aportar?{% else %}Got something to add?{% endif %}
</span>
<span class="pe-cta-sub">
{% if lang == "es" %}Cuéntame qué opinas, qué sugieres, o si seguimos explorando este tema.{% else %}Tell me what you think, what you'd suggest, or whether we should keep exploring this topic.{% endif %}
</span>
</div>
<button type="button" class="pe-cta-btn"
hx-get="/api/htmx/post/{{ item.id }}/message-form"
hx-vals='{"title":"{{ item.title }}","lang":"{{ lang }}"}'
hx-target="#modal-slot"
hx-swap="innerHTML">
<span aria-hidden="true">💬</span> {% if lang == "es" %}Enviar comentario{% else %}Send a comment{% endif %}
</button>
</div>
{# Views — subtle line; beacon fires once on load and fills #post-views. #}
<div class="pe-views-line">
<span id="post-views"
hx-post="/api/htmx/post/{{ item.id }}/view"
hx-trigger="load"
hx-vals='{"lang":"{{ lang }}"}'
hx-swap="innerHTML">·</span>
<span>{% if lang == "es" %}lecturas{% else %}reads{% endif %}</span>
</div>
</section>
{% include "partials/cta/subscribe.j2" %}
{% if item.translations and item.translations | length > 1 %}
<footer class="mt-12 pt-6 border-t border-gray-200 dark:border-gray-700">
<p class="text-sm ds-text-muted">
{% if lang == "es" %}También disponible en:{% else %}Also available in:{% endif %}
{% for tlang in item.translations %}
{% if tlang != lang %}
{% set tslug = item.translation_slugs[tlang] %}
<a href="/{{ tlang }}/{{ item.content_type }}/{{ tslug }}"
class="underline hover:ds-text"
hx-get="/{{ tlang }}/{{ item.content_type }}/{{ tslug }}"
hx-target="#main-content"
hx-swap="innerHTML"
hx-push-url="true">{{ tlang | upper }}</a>
{% endif %}
{% endfor %}
</p>
</footer>
{% endif %}
</article>
{% include "partials/image-zoom.j2" %}

View file

@ -0,0 +1,211 @@
{# ontoref Services — tier-structured.
The protocol is a gift (voluntary-adoption, protocol-not-runtime); what is
offered is help OPERATING its highest tier (tier-2: operations, witness,
governance — via-001) and TRAINING/COACHING to climb the lower tiers (tier-0
declarative, tier-1 substrate). Bilingual via `texts[...]`; ES deep-link
anchors via data-anchor-es. Mirrors the jpl services design (ds-* classes). #}
<div class="min-h-screen ds-bg-page">
<section class="py-8 ds-container">
<div class="mx-auto max-w-4xl text-center">
<h1 class="text-balance text-4xl font-bold tracking-tight ds-text sm:text-6xl">
{{ texts["services-page-title"] }}
</h1>
<p class="mt-ds-6 ds-body ds-text-secondary max-w-2xl mx-auto">
{{ texts["services-page-subtitle"] }}
</p>
<p class="mt-3 ds-caption font-mono ds-text-secondary max-w-2xl mx-auto">
{{ texts["services-page-note"] }}
</p>
</div>
</section>
{# ── Tier 2: the proposal — operate the operations/governance surface ─────── #}
<section id="tier2" data-anchor-es="tier2" class="py-16 ds-bg">
<div class="ds-container-lg ds-container">
<div class="text-center mb-ds-12">
<span class="ds-caption font-mono px-2 py-1 ds-bg-page border ds-border ds-rounded ds-text-secondary">
{{ texts["services-tier2-eyebrow"] }}
</span>
<h2 class="mt-4 text-3xl font-bold ds-text">{{ texts["services-tier2-title"] }}</h2>
<p class="mt-4 ds-text-secondary max-w-3xl mx-auto">
{{ texts["services-tier2-desc"] }}
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm">
<h3 class="ds-heading-4 ds-text mb-3">{{ texts["services-tier2-ops-title"] }}</h3>
<p class="ds-text-secondary">{{ texts["services-tier2-ops-desc"] }}</p>
</div>
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm">
<h3 class="ds-heading-4 ds-text mb-3">{{ texts["services-tier2-audit-title"] }}</h3>
<p class="ds-text-secondary">{{ texts["services-tier2-audit-desc"] }}</p>
</div>
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm">
<h3 class="ds-heading-4 ds-text mb-3">{{ texts["services-tier2-validators-title"] }}</h3>
<p class="ds-text-secondary">{{ texts["services-tier2-validators-desc"] }}</p>
</div>
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm">
<h3 class="ds-heading-4 ds-text mb-3">{{ texts["services-tier2-migration-title"] }}</h3>
<p class="ds-text-secondary">{{ texts["services-tier2-migration-desc"] }}</p>
</div>
</div>
<div class="mt-16">
<h3 class="text-2xl font-bold ds-text text-center mb-8">{{ texts["services-what-you-get"] }}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
<div class="flex items-start gap-3"><span class="ds-text-secondary">{{ texts["services-get-witnessed-history"] }}</span></div>
<div class="flex items-start gap-3"><span class="ds-text-secondary">{{ texts["services-get-validators"] }}</span></div>
<div class="flex items-start gap-3"><span class="ds-text-secondary">{{ texts["services-get-knowledge-transfer"] }}</span></div>
<div class="flex items-start gap-3"><span class="ds-text-secondary">{{ texts["services-get-no-lockin"] }}</span></div>
<div class="flex items-start gap-3 md:col-span-2"><span class="ds-text-secondary">{{ texts["services-get-open-protocol"] }}</span></div>
</div>
</div>
</div>
</section>
{# ── Training & coaching for the lower tiers — use or learn ───────────────── #}
<section id="learn" data-anchor-es="aprender" class="py-16 ds-bg-page">
<div class="ds-container-lg ds-container">
<div class="text-center mb-ds-12">
{% set training_img = "ontoref-formacion" if lang == "es" else "ontoref-training" %}
<picture>
<source srcset="/images/{{ training_img }}.avif" type="image/avif">
<source srcset="/images/{{ training_img }}.webp" type="image/webp">
<img src="/images/{{ training_img }}.webp"
alt="{{ texts['services-learn-title'] }}"
width="1536" height="512" loading="lazy"
class="mb-ds-8 mx-auto rounded-lg ds-shadow-sm w-full max-w-3xl h-auto" />
</picture>
<h2 class="text-3xl font-bold ds-text">{{ texts["services-learn-title"] }}</h2>
<p class="mt-4 ds-text-secondary max-w-3xl mx-auto">{{ texts["services-learn-desc"] }}</p>
{% set coaching_img = "ontoref-coaching-es" if lang == "es" else "ontoref-coaching-en" %}
<picture>
<source srcset="/images/{{ coaching_img }}.avif" type="image/avif">
<source srcset="/images/{{ coaching_img }}.webp" type="image/webp">
<img src="/images/{{ coaching_img }}.webp"
alt="{{ texts['services-learn-title'] }}"
width="1536" height="1024" loading="lazy"
class="mt-ds-8 mx-auto rounded-lg ds-shadow-sm w-full max-w-3xl h-auto" />
</picture>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div id="coaching" data-anchor-es="coaching">
<span class="ds-caption font-mono ds-text-secondary">{{ texts["services-coaching-eyebrow"] }}</span>
<h3 class="text-2xl font-bold ds-text mt-2 mb-6">{{ texts["services-coaching-title"] }}</h3>
<p class="ds-text-secondary mb-8">{{ texts["services-coaching-desc"] }}</p>
<div class="space-y-3">
<div class="ds-bg p-ds-4 ds-rounded-lg">
<div class="flex justify-between items-center mb-2">
<h4 class="font-semibold ds-text">{{ texts["services-coaching-1to1-title"] }}</h4>
<span class="text-sm ds-text-secondary">{{ texts["services-coaching-1to1-note"] }}</span>
</div>
<p class="text-sm ds-text-secondary">{{ texts["services-coaching-1to1-desc"] }}</p>
</div>
<div class="ds-bg p-ds-4 ds-rounded-lg">
<div class="flex justify-between items-center mb-2">
<h4 class="font-semibold ds-text">{{ texts["services-coaching-ondaod-title"] }}</h4>
<span class="text-sm ds-text-secondary">{{ texts["services-coaching-ondaod-note"] }}</span>
</div>
<p class="text-sm ds-text-secondary">{{ texts["services-coaching-ondaod-desc"] }}</p>
</div>
<div class="ds-bg p-ds-4 ds-rounded-lg">
<div class="flex justify-between items-center mb-2">
<h4 class="font-semibold ds-text">{{ texts["services-coaching-adr-title"] }}</h4>
<span class="text-sm ds-text-secondary">{{ texts["services-coaching-adr-note"] }}</span>
</div>
<p class="text-sm ds-text-secondary">{{ texts["services-coaching-adr-desc"] }}</p>
</div>
<div class="ds-bg p-ds-4 ds-rounded-lg">
<div class="flex justify-between items-center mb-2">
<h4 class="font-semibold ds-text">{{ texts["services-coaching-ongoing-title"] }}</h4>
<span class="text-sm ds-text-secondary">{{ texts["services-coaching-ongoing-note"] }}</span>
</div>
<p class="text-sm ds-text-secondary">{{ texts["services-coaching-ongoing-desc"] }}</p>
</div>
</div>
</div>
<div id="training" data-anchor-es="formacion">
<span class="ds-caption font-mono ds-text-secondary">{{ texts["services-training-eyebrow"] }}</span>
<h3 class="text-2xl font-bold ds-text mt-2 mb-6">{{ texts["services-training-title"] }}</h3>
<p class="ds-text-secondary mb-8">{{ texts["services-training-desc"] }}</p>
<div class="space-y-3">
<div class="ds-bg p-ds-4 ds-rounded-lg">
<h4 class="font-semibold ds-text mb-2">{{ texts["services-training-courses-title"] }}</h4>
<p class="text-sm ds-text-secondary">{{ texts["services-training-courses-desc"] }}</p>
</div>
<div class="ds-bg p-ds-4 ds-rounded-lg">
<h4 class="font-semibold ds-text mb-2">{{ texts["services-training-workshops-title"] }}</h4>
<p class="text-sm ds-text-secondary">{{ texts["services-training-workshops-desc"] }}</p>
</div>
<div class="ds-bg p-ds-4 ds-rounded-lg">
<h4 class="font-semibold ds-text mb-2">{{ texts["services-training-talks-title"] }}</h4>
<p class="text-sm ds-text-secondary">{{ texts["services-training-talks-desc"] }}</p>
</div>
<div class="ds-bg p-ds-4 ds-rounded-lg">
<h4 class="font-semibold ds-text mb-2">{{ texts["services-training-content-title"] }}</h4>
<p class="text-sm ds-text-secondary">{{ texts["services-training-content-desc"] }}</p>
</div>
</div>
</div>
</div>
</div>
</section>
{# ── The three tiers — which one fits (taglines from tier-taglines.ncl) ───── #}
<section class="py-16 ds-bg">
<div class="ds-container-lg ds-container">
<div class="text-center mb-ds-12">
<h2 class="text-3xl font-bold ds-text">{{ texts["services-tiers-title"] }}</h2>
<p class="mt-4 ds-text-secondary max-w-3xl mx-auto">{{ texts["services-tiers-desc"] }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm">
<span class="ds-caption font-mono ds-text-secondary">{{ texts["services-tier0-label"] }}</span>
<h3 class="ds-heading-4 ds-text mt-2 mb-2">{{ texts["services-tier0-title"] }}</h3>
<p class="ds-text-secondary text-sm">{{ texts["services-tier0-desc"] }}</p>
</div>
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm">
<span class="ds-caption font-mono ds-text-secondary">{{ texts["services-tier1-label"] }}</span>
<h3 class="ds-heading-4 ds-text mt-2 mb-2">{{ texts["services-tier1-title"] }}</h3>
<p class="ds-text-secondary text-sm">{{ texts["services-tier1-desc"] }}</p>
</div>
<div class="ds-bg-page p-ds-6 ds-rounded-lg ds-shadow-sm border ds-border">
<span class="ds-caption font-mono ds-text-secondary">{{ texts["services-tier2-label"] }}</span>
<h3 class="ds-heading-4 ds-text mt-2 mb-2">{{ texts["services-tier2-card-title"] }}</h3>
<p class="ds-text-secondary text-sm">{{ texts["services-tier2-card-desc"] }}</p>
</div>
</div>
</div>
</section>
<section class="py-16 ds-bg-page">
<div class="mx-auto max-w-4xl ds-container text-center">
{% set action_img = "ontoref-accion" if lang == "es" else "ontoref-action" %}
<picture>
<source srcset="/images/{{ action_img }}.avif" type="image/avif">
<source srcset="/images/{{ action_img }}.webp" type="image/webp">
<img src="/images/{{ action_img }}.webp"
alt="{{ texts['services-cta-title'] }}"
width="1536" height="512" loading="lazy"
class="mb-ds-8 mx-auto rounded-lg ds-shadow-sm w-full max-w-3xl h-auto" />
</picture>
<h2 class="text-3xl font-bold ds-text mb-ds-4">{{ texts["services-cta-title"] }}</h2>
<p class="ds-text-secondary mb-8 ds-body">{{ texts["services-cta-desc"] }}</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ texts['services-cta-primary-url'] | default('/contact') }}" class="no-underline ds-btn-primary">
{{ texts["services-cta-primary"] }}
</a>
<a href="{{ texts['services-cta-secondary-url'] | default('/about') }}" class="no-underline ds-btn-secondary">
{{ texts["services-cta-secondary"] }}
</a>
</div>
</div>
</section>
</div>
{% include "partials/image-zoom.j2" %}

View file

@ -0,0 +1,31 @@
<article class="ds-page ds-page-{{ page_id }}" data-page-id="{{ page_id }}">
<header class="ds-page-header">
<div class="ds-container">
<h1 class="ds-heading-xl">{{ texts[page_id ~ "-page-title"] | default(page_id) }}</h1>
{% if texts[page_id ~ "-page-subtitle"] %}
<p class="ds-text-lead">{{ texts[page_id ~ "-page-subtitle"] | safe }}</p>
{% endif %}
</div>
</header>
<div class="ds-page-body">
<div class="ds-container">
{# Each page's content is driven by its FTL translations. #}
{# Common structure: description paragraph + sections keyed by page_id prefix. #}
{% if texts[page_id ~ "-page-description"] %}
<p class="ds-text-lead ds-text-muted">{{ texts[page_id ~ "-page-description"] }}</p>
{% endif %}
{# Product pages: show a minimal overview with a "learn more" link pattern. #}
{% set products = ["syntaxis", "vapora", "rustelo", "provisioning", "stratumiops", "kogral", "typedialog", "ontoref", "secretumvault"] %}
{% if page_id in products %}
{% include "partials/product_page.j2" %}
{% endif %}
{# Auth pages: show the login form fragment inline. #}
{% if page_id == "login" %}
{% include "partials/login_form.j2" %}
{% endif %}
</div>
</div>
</article>

View file

@ -0,0 +1,43 @@
<form id="login-form"
hx-post="/api/htmx/auth/otp/verify"
hx-target="#login-form"
hx-swap="outerHTML"
class="flex flex-col gap-4">
<h2 class="text-xl font-semibold text-base-content">{{ check_inbox }}</h2>
<p class="text-sm text-base-content/70">{{ sent_to | safe }}</p>
{% if error %}
<p class="text-error text-sm" role="alert">{{ error }}</p>
{% endif %}
<input type="hidden" name="email" value="{{ email }}">
<input type="hidden" name="lang" value="{{ lang }}">
{% if return_url %}<input type="hidden" name="return_url" value="{{ return_url }}">{% endif %}
<input type="text"
name="code"
id="auth-otp-code"
inputmode="numeric"
autocomplete="one-time-code"
aria-label="Verification code"
placeholder="{{ placeholder }}"
maxlength="6"
required
class="input input-bordered w-full text-center tracking-widest text-xl">
<button type="submit" class="btn btn-primary w-full">{{ verify_btn }}</button>
<button type="button"
class="btn btn-sm w-full border border-base-content/20 text-base-content/60 bg-transparent hover:bg-base-content/5"
hx-post="/api/htmx/auth/otp/request"
hx-include="[name=email],[name=lang],[name=return_url]"
hx-target="#login-form"
hx-swap="outerHTML">{{ resend_btn }}</button>
<button type="button"
class="btn btn-ghost btn-xs dark:text-gray-400 text-base-content/50 mt-1"
hx-get="{{ back_btn_url }}"
hx-target="#modal-slot"
hx-swap="outerHTML">{{ back_btn }}</button>
</form>

View file

@ -0,0 +1,26 @@
<form id="login-form"
hx-post="/api/htmx/auth/otp/request"
hx-target="#login-form"
hx-swap="outerHTML"
class="flex flex-col gap-4">
<p class="text-sm text-base-content/70">{{ subtitle }}</p>
{% if error %}
<p class="text-error text-sm" role="alert">{{ error }}</p>
{% endif %}
<input type="hidden" name="lang" value="{{ lang }}">
{% if return_url %}<input type="hidden" name="return_url" value="{{ return_url }}">{% endif %}
<input type="email"
name="email"
id="auth-email"
autocomplete="email"
aria-label="Email address"
placeholder="{{ placeholder }}"
value="{{ email | default('') }}"
required
class="input input-bordered w-full">
<button type="submit" class="btn btn-primary w-full">{{ continue_btn }}</button>
</form>

View file

@ -0,0 +1,39 @@
<form id="login-form"
hx-post="/api/htmx/auth/otp/gdpr"
hx-target="#login-form"
hx-swap="outerHTML"
class="flex flex-col gap-4">
<h2 class="text-xl font-semibold text-base-content">{{ gdpr_title }}</h2>
<p class="text-sm text-base-content/70">{{ gdpr_description }}</p>
{% if error %}
<p class="text-error text-sm" role="alert">{{ error }}</p>
{% endif %}
<input type="hidden" name="pending_token" value="{{ pending_token }}">
{% if return_url %}<input type="hidden" name="return_url" value="{{ return_url }}">{% endif %}
<label class="flex items-start gap-3 cursor-pointer">
<input type="checkbox"
id="gdpr-required"
name="gdpr_required"
value="1"
required
class="w-4 h-4 mt-1 shrink-0 cursor-pointer rounded"
style="accent-color:hsl(var(--p))">
<span class="text-sm text-base-content">{{ gdpr_required_label | safe }}</span>
</label>
<label class="flex items-start gap-3 cursor-pointer">
<input type="checkbox"
id="gdpr-marketing"
name="gdpr_marketing"
value="1"
class="w-4 h-4 mt-1 shrink-0 cursor-pointer rounded"
style="accent-color:hsl(var(--p))">
<span class="text-sm text-base-content/70">{{ gdpr_marketing_label }}</span>
</label>
<button type="submit" class="btn btn-primary w-full">{{ gdpr_submit_btn }}</button>
</form>

View file

@ -0,0 +1,25 @@
<div id="modal-slot">
<div class="fixed inset-0 z-[80] bg-black opacity-75 backdrop-blur-sm cursor-pointer"
hx-get="/api/htmx/auth/close"
hx-target="#modal-slot"
hx-swap="outerHTML"
aria-hidden="true"></div>
<div class="fixed inset-0 z-[85] flex items-center justify-center p-4 pointer-events-none">
<div role="dialog"
aria-modal="true"
aria-labelledby="login-modal-title"
class="relative w-full max-w-md bg-base-100 rounded-2xl shadow-xl p-8 pointer-events-auto">
<div class="flex justify-between items-center mb-6">
<h2 id="login-modal-title" class="text-xl font-semibold ds-text">{{ title }}</h2>
<button type="button"
class="absolute top-4 right-4 btn btn-ghost btn-sm btn-circle"
hx-get="/api/htmx/auth/close"
hx-target="#modal-slot"
hx-swap="outerHTML"
aria-label="{{ close_label }}"
title="{{ close_label }}">✕</button>
</div>
{{ form_html | safe }}
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
<article data-stub="card" id="card-{{ id }}" class="ds-card">
<h3>{{ title }}</h3>
</article>

View file

@ -0,0 +1,18 @@
<section data-grid-host="{{ kind }}" data-loading-states>
<form class="ds-grid-filters"
hx-get="{{ endpoint }}"
hx-target="closest [data-grid-host] > [data-content-grid]"
hx-swap="morph"
hx-trigger="keyup changed delay:300ms from:input[name=q], change from:select[name=tag], submit"
hx-push-url="true"
hx-include="this"
hx-sync="this:replace">
<input type="search" name="q" placeholder="Search…" aria-label="Search"
autocomplete="off" data-loading-disable>
<select name="tag" aria-label="Filter by tag" data-loading-disable>
<option value="">All tags</option>
</select>
<span class="ds-loading-indicator" data-loading aria-hidden="true">…</span>
</form>
{{ initial_grid_html | safe }}
</section>

View file

@ -0,0 +1,33 @@
{# Shared header for content items: title, subtitle, meta row, tags. #}
{# Expected context vars: item (UnifiedContentItem fields), lang #}
<header class="mb-8">
<h1 class="text-3xl font-bold ds-text mb-2">{{ item.title }}</h1>
{% if item.subtitle %}
<p class="text-xl ds-text-muted mb-4">{{ item.subtitle }}</p>
{% endif %}
<div class="flex flex-wrap items-center gap-3 text-sm ds-text-muted mb-4">
{% if item.author %}
<span>{{ item.author }}</span>
<span aria-hidden="true">·</span>
{% endif %}
{% if item.created_at %}
<time datetime="{{ item.created_at }}">{{ item.created_at | truncate_words(1) }}</time>
{% endif %}
{% if item.read_time %}
<span aria-hidden="true">·</span>
<span>{{ item.read_time }}</span>
{% endif %}
</div>
{% if item.tags %}
<div class="flex flex-wrap gap-2" aria-label="Tags">
{% for tag in item.tags %}
<a href="/{{ item.content_type }}?tag={{ tag | slugify }}"
class="px-2 py-0.5 text-xs rounded-full border border-current ds-text-muted hover:ds-text transition-colors no-underline"
hx-get="/{{ item.content_type }}?tag={{ tag | slugify }}"
hx-target="#main-content"
hx-swap="innerHTML"
hx-push-url="true">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
</header>

View file

@ -0,0 +1,106 @@
{# Subscribe CTA — config-driven newsletter call-to-action.
Banner (heading/subtext + buttons) rendered at the foot of content grids and
below post ratings. The primary button opens a lightbox <dialog> holding the
listmonk subscription form, which POSTs straight to `newsletter.action` for
the single list `newsletter.list_uuid`. Endpoint/list/copy come from the
`newsletter` global (htmx_env.rs ← site.ncl's sibling newsletter.ncl); nothing
listmonk-specific is hardcoded here except listmonk's own fixed field names
(email, name, nonce honeypot, l, consent). The listmonk host must be allowed
in the CSP form-action or the browser blocks the POST. #}
{% if newsletter and newsletter.enabled and newsletter.action %}
{% set nl_heading = newsletter.heading[lang] or newsletter.heading['en'] %}
{% set nl_subtext = newsletter.subtext[lang] or newsletter.subtext['en'] %}
{% set nl_cta = newsletter.cta_label[lang] or newsletter.cta_label['en'] %}
{% set f = newsletter.form %}
<aside class="nl-cta" aria-label="{{ nl_heading }}">
<div class="nl-cta__body">
<h2 class="nl-cta__title">{{ nl_heading }}</h2>
<p class="nl-cta__sub">{{ nl_subtext }}</p>
</div>
<div class="nl-cta__actions">
<button type="button" class="nl-cta__btn nl-cta__btn--primary" data-nl-open>
{% if newsletter.icon %}<span aria-hidden="true">{{ newsletter.icon }}</span> {% endif %}{{ nl_cta }}
</button>
{% if newsletter.secondary and newsletter.secondary.href %}
{% set nl_sec = newsletter.secondary.label[lang] or newsletter.secondary.label['en'] %}
<a class="nl-cta__btn nl-cta__btn--ghost"
href="{{ newsletter.secondary.href }}"
hx-get="{{ newsletter.secondary.href }}"
hx-target="#main-content" hx-swap="innerHTML" hx-push-url="true">{{ nl_sec }}</a>
{% endif %}
</div>
<dialog class="nl-modal" aria-label="{{ f.title[lang] or f.title['en'] }}">
{# hx-boost="false" + hx-disable: this form must POST natively cross-origin to
listmonk. Without it, a page-level hx-boost hijacks the submit and htmx
rejects the external URL (htmx:invalidPath) — the POST never fires. #}
<form class="nl-modal__card" method="post" action="{{ newsletter.action }}"
hx-boost="false" hx-disable>
<button type="button" class="nl-modal__close" data-nl-close
aria-label="{{ f.close_label[lang] or f.close_label['en'] }}">&times;</button>
<h2 class="nl-modal__title">{{ f.title[lang] or f.title['en'] }}</h2>
<label class="nl-field">
<span class="nl-field__lbl">Email</span>
<input type="email" name="email" required autocomplete="email"
placeholder="{{ f.email_ph[lang] or f.email_ph['en'] }}">
</label>
<label class="nl-field">
<span class="nl-field__lbl">{{ f.name_ph[lang] or f.name_ph['en'] }}</span>
<input type="text" name="name" autocomplete="name"
placeholder="{{ f.name_ph[lang] or f.name_ph['en'] }}">
</label>
{# listmonk honeypot: MUST stay empty and hidden. #}
<input type="text" name="nonce" value="" tabindex="-1" autocomplete="off" aria-hidden="true"
style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0">
{# single target list #}
<input type="hidden" name="l" value="{{ newsletter.list_uuid }}">
<label class="nl-consent">
<input type="checkbox" name="consent" value="1" required data-nl-consent>
<span>{{ f.consent_pre[lang] or f.consent_pre['en'] }}<a
href="{{ f.privacy_href[lang] or f.privacy_href['en'] }}"
target="_blank" rel="noopener">{{ f.privacy_label[lang] or f.privacy_label['en'] }}</a>{{ f.consent_post[lang] or f.consent_post['en'] }}</span>
</label>
{# disabled until consent is ticked (the JS toggles it); `required` is the
native backstop. #}
<button type="submit" class="nl-cta__btn nl-cta__btn--primary nl-modal__submit" data-nl-submit disabled>
{{ f.submit[lang] or f.submit['en'] }}
</button>
</form>
</dialog>
</aside>
<script>
/* Delegated + globally-guarded so it survives HTMX swaps (where inline scripts
re-run and document.currentScript is null) without stacking listeners. */
(function(){
if(window.__nlCtaWired) return;
window.__nlCtaWired = true;
document.addEventListener('click', function(ev){
var t = ev.target;
if(!t || !t.closest) return;
var open = t.closest('[data-nl-open]');
if(open){
var dlg = open.closest('.nl-cta') && open.closest('.nl-cta').querySelector('dialog.nl-modal');
if(dlg && typeof dlg.showModal === 'function' && !dlg.open) dlg.showModal();
return;
}
var close = t.closest('[data-nl-close]');
if(close){ var d = close.closest('dialog.nl-modal'); if(d) d.close(); return; }
// Backdrop click (lands on the dialog element itself, not the inner card).
if(t.tagName === 'DIALOG' && t.classList.contains('nl-modal')) t.close();
});
// Submit button stays disabled until the consent box is ticked.
document.addEventListener('change', function(ev){
var t = ev.target;
if(!t || !t.closest || !t.matches('[data-nl-consent]')) return;
var form = t.closest('form');
var submit = form && form.querySelector('[data-nl-submit]');
if(submit) submit.disabled = !t.checked;
});
})();
</script>
{% endif %}

View file

@ -0,0 +1,113 @@
{# Global in-page lightbox. Click to zoom, full-screen modal overlay.
Handles two media kinds via event delegation:
• <picture> images → shown as <img> (browser-picked AVIF/WebP, currentSrc)
• .content-graph-mini-preview → its inline ego-network <svg>, cloned & enlarged
Boost-proof: htmx navigation can drop the overlay node, so styles + overlay are
recreated ON DEMAND (looked up by id, rebuilt if missing) rather than cached in
closure refs. Listeners bind to `document` exactly once (document survives htmx
swaps), guarded by a window flag so re-running after a swap never double-binds. #}
<script>
(function () {
var STYLE_ID = 'oz-style';
var OVERLAY_ID = 'oz-overlay';
function ensureStyle() {
if (document.getElementById(STYLE_ID)) return;
var s = document.createElement('style');
s.id = STYLE_ID;
s.textContent = ''
+ '.oz-overlay{position:fixed;inset:0;z-index:2147483000;display:none;'
+ 'align-items:center;justify-content:center;background:rgba(8,11,16,.92);'
+ 'padding:2rem;cursor:zoom-out;opacity:0;transition:opacity .15s ease}'
+ '.oz-overlay.oz-open{display:flex;opacity:1}'
+ '.oz-overlay img.oz-media{max-width:100%;max-height:100%;width:auto;height:auto;'
+ 'border-radius:.5rem;box-shadow:0 10px 40px rgba(0,0,0,.5);cursor:default}'
+ '.oz-overlay svg.oz-media{width:min(90vw,90vh);height:min(90vw,90vh);'
+ 'max-width:100%;max-height:100%;cursor:default}'
+ '.oz-close{position:absolute;top:1rem;right:1.25rem;font-size:2.25rem;'
+ 'line-height:1;color:#fff;background:none;border:0;cursor:pointer;opacity:.85}'
+ '.oz-close:hover{opacity:1}'
+ 'picture img{cursor:zoom-in}';
(document.head || document.documentElement).appendChild(s);
}
function ensureOverlay() {
var o = document.getElementById(OVERLAY_ID);
if (o) return o;
o = document.createElement('div');
o.id = OVERLAY_ID;
o.className = 'oz-overlay';
o.setAttribute('role', 'dialog');
o.setAttribute('aria-modal', 'true');
o.innerHTML = '<button class="oz-close" type="button" aria-label="Close">×</button>';
document.body.appendChild(o);
return o;
}
function setMedia(node) {
ensureStyle();
var o = ensureOverlay();
var old = o.querySelector('.oz-media');
if (old) old.remove();
node.classList.add('oz-media');
o.appendChild(node);
o.classList.add('oz-open');
document.body.style.overflow = 'hidden';
}
function openImg(src, alt) {
var im = new Image();
im.src = src;
im.alt = alt || '';
setMedia(im);
}
function openGraph(svgEl) {
var c = svgEl.cloneNode(true);
// Overlay is always dark — force the on-dark palette so labels/edges stay
// legible even when the page itself is in light mode.
c.style.setProperty('--cg-label', '#d1d5db');
c.style.setProperty('--cg-edge', '#4b5563');
c.style.setProperty('--cg-node', '#9ca3af');
setMedia(c);
}
function closeImg() {
var o = document.getElementById(OVERLAY_ID);
if (o) o.classList.remove('oz-open');
document.body.style.overflow = '';
}
if (!window.__ozBound) {
window.__ozBound = true;
document.addEventListener('click', function (e) {
var t = e.target;
if (!t || !t.closest) return;
var img = t.closest('picture img');
if (img) {
e.preventDefault();
openImg(img.currentSrc || img.src, img.alt);
return;
}
var mini = t.closest('.content-graph-mini-preview');
if (mini) {
var svg = mini.querySelector('svg');
if (svg) {
e.preventDefault();
openGraph(svg);
return;
}
}
var ov = t.closest('#' + OVERLAY_ID);
if (ov && (t.id === OVERLAY_ID || t.closest('.oz-close'))) closeImg();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' || e.key === 'Esc') closeImg();
});
}
})();
</script>

View file

@ -0,0 +1,38 @@
<div class="ds-auth-form">
<form hx-post="/api/auth/login"
hx-target="#login-result"
hx-swap="innerHTML"
class="ds-form">
<div class="ds-form-group">
<label for="login-email" class="ds-label">{{ texts["auth-email-address"] }}</label>
<input id="login-email"
name="email"
type="email"
class="ds-input"
placeholder="{{ texts['auth-enter-email'] }}"
required
autocomplete="email" />
</div>
<div class="ds-form-group">
<label for="login-password" class="ds-label">{{ texts["auth-password"] }}</label>
<input id="login-password"
name="password"
type="password"
class="ds-input"
placeholder="{{ texts['auth-enter-password'] }}"
required
autocomplete="current-password" />
</div>
<div class="ds-form-row ds-justify-between ds-align-center">
<label class="ds-checkbox-label">
<input type="checkbox" name="remember_me" class="ds-checkbox" />
{{ texts["auth-remember-me"] }}
</label>
<a href="#" class="ds-link ds-text-sm">{{ texts["auth-forgot-password"] }}</a>
</div>
<button type="submit" class="ds-button ds-button-primary ds-w-full">
{{ texts["auth-sign-in"] }}
</button>
<div id="login-result"></div>
</form>
</div>

View file

@ -0,0 +1,23 @@
<details id="lang-selector" class="language-selector relative">
<summary class="btn btn-ghost btn-sm btn-icon ds-text hover:ds-text-secondary flex items-center justify-center text-sm cursor-pointer list-none"
aria-label="Language">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"></path>
</svg>
</summary>
<div class="absolute right-0 mt-1 w-48 ds-bg border border-gray-500 rounded-md shadow-lg z-10"
role="menu" aria-label="Language selection">
<div class="py-1">
{% for lang in languages %}
<button type="button"
class="block w-full text-left px-3 py-2 hover:ds-bg-secondary{% if lang.is_active %} font-semibold{% endif %}"
hx-post="{{ lang.endpoint }}"
hx-target="#lang-selector"
hx-swap="outerHTML"
{% if lang.is_active %}aria-current="true" disabled{% endif %}
data-lang="{{ lang.code }}">{{ lang.label }}</button>
{% endfor %}
</div>
</div>
</details>

View file

@ -0,0 +1,5 @@
<button type="button"
class="ds-btn-ghost-sm"
hx-get="/api/htmx/auth/login-modal?lang={{ lang }}"
hx-target="#modal-slot"
hx-swap="outerHTML">{{ label }}</button>

View file

@ -0,0 +1,5 @@
<button type="button"
class="ds-btn-ghost-sm"
hx-post="/api/htmx/auth/logout"
hx-target="body"
hx-swap="outerHTML">{{ label }}</button>

View file

@ -0,0 +1,20 @@
<button type="button"
class="ds-theme-toggle"
hx-post="/api/htmx/theme/toggle"
hx-swap="outerHTML"
hx-target="this"
hx-ext="multi-swap"
data-theme="{{ theme }}"
aria-label="{{ label }}">
{% if theme == "dark" %}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
{% else %}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
{% endif %}
</button>

View file

@ -0,0 +1,28 @@
<div id="cookie-consent-wrapper">
<div class="fixed inset-0 z-50 bg-black opacity-75"></div>
<div id="cookie-banner"
role="dialog"
aria-label="Cookie consent"
class="fixed bottom-0 left-0 right-0 z-[60] bg-base-100 border-t-2 border-base-300 shadow-2xl px-6 py-5 flex flex-col sm:flex-row items-start sm:items-center gap-4">
<p class="text-sm flex-1 ds-text">
{{ message }}
<a href="{{ policy_href }}" class="underline ds-text">{{ policy_link }}</a>
{{ policy_suffix }}
</p>
<div class="flex items-center gap-2 shrink-0">
<a href="{{ policy_href }}" class="btn btn-ghost btn-sm rounded-full ds-text no-underline">{{ manage_label }}</a>
<button type="button"
hx-post="{{ endpoint }}"
hx-vals='{"level":"essential"}'
hx-target="#cookie-consent-wrapper"
hx-swap="outerHTML"
class="btn btn-ghost btn-sm rounded-full ds-text">{{ reject_label }}</button>
<button type="button"
hx-post="{{ endpoint }}"
hx-vals='{"level":"all"}'
hx-target="#cookie-consent-wrapper"
hx-swap="outerHTML"
class="btn btn-neutral btn-sm rounded-full">{{ accept_label }}</button>
</div>
</div>
</div>

View file

@ -0,0 +1 @@
<div id="modal-slot" aria-live="polite"></div>

View file

@ -0,0 +1,7 @@
<div id="toast-stack"
class="fixed bottom-4 right-4 flex flex-col gap-2 z-50"
aria-live="polite"
hx-ext="sse"
sse-connect="{{ sse_url }}"
sse-swap="toast"
hx-swap="beforeend"></div>

View file

@ -0,0 +1,30 @@
<div class="ds-product-page">
{% if texts[page_id ~ "-badge"] %}
<span class="ds-badge">{{ texts[page_id ~ "-badge"] }}</span>
{% endif %}
{% if texts[page_id ~ "-tagline"] %}
<p class="ds-tagline">{{ texts[page_id ~ "-tagline"] }}</p>
{% endif %}
{% if texts[page_id ~ "-hero-title"] %}
<h2 class="ds-heading-lg">{{ texts[page_id ~ "-hero-title"] }}</h2>
{% endif %}
{% if texts[page_id ~ "-hero-subtitle"] %}
<p class="ds-text-lead">{{ texts[page_id ~ "-hero-subtitle"] | safe }}</p>
{% endif %}
<div class="ds-product-actions">
{% if texts[page_id ~ "-cta-github"] %}
<a href="#" class="ds-button ds-button-outline ds-button-sm">
{{ texts[page_id ~ "-cta-github"] }}
</a>
{% endif %}
{% if texts[page_id ~ "-cta-get-started"] %}
<a href="/contact" class="ds-button ds-button-primary ds-button-sm">
{{ texts[page_id ~ "-cta-get-started"] }}
</a>
{% endif %}
</div>
</div>

View file

@ -0,0 +1,72 @@
<footer class="ds-bg-surface border-t border-gray-200 mt-auto">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="col-span-1">
{% if logo.show_in_footer %}
{% if logo.has_dark_variant %}
<a href="/" class="inline-block mb-4">
<img src="{{ logo.light_src }}" alt="{{ logo.alt }}" class="h-8 w-auto dark:hidden">
<img src="{{ logo.dark_src }}" alt="{{ logo.alt }}" class="h-8 w-auto hidden dark:block">
</a>
{% else %}
<a href="/" class="inline-block mb-4">
<img src="{{ logo.light_src }}" alt="{{ logo.alt }}" class="h-8 w-auto">
</a>
{% endif %}
{% endif %}
<p class="ds-text-secondary text-sm mb-4">{{ company_desc }}</p>
<p class="text-sm ds-text-secondary">{{ copyright_text }}</p>
</div>
{% for section in sections %}
<div>
<h4 class="font-semibold ds-text mb-3">{{ section.title }}</h4>
<ul class="space-y-2 text-sm">
{% for link in section.links %}
<li>
<a href="{{ link.href }}"
class="ds-text-secondary hover:ds-text transition-colors no-underline"
{% if link.is_external %}target="_blank" rel="noopener noreferrer"{% endif %}>
{{ link.label }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% if social_links %}
<div>
<h4 class="font-semibold ds-text mb-3">{{ social_title }}</h4>
<div class="flex flex-col space-y-3">
{% for social in social_links %}
<a href="{{ social.url }}"
target="_blank"
rel="noopener noreferrer"
class="ds-text-secondary hover:ds-text transition-colors"
aria-label="{{ social.name }}">{{ social.icon_html | safe }}</a>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<div class="border-t border-base-300 mt-8 pt-5 flex justify-center items-center gap-2 text-sm ds-text-secondary">
<span>{{ built_label }}</span>
<a href="/rustelo" class="inline-flex items-center gap-1.5 hover:opacity-80 transition-opacity no-underline">
<img src="/images/logos/rustelo-img.svg" alt="Rustelo" class="h-5 w-auto">
<span class="text-gray-700 dark:text-gray-500">Rustelo</span>
</a>
</div>
</div>
</footer>
<button class="fixed bottom-4 right-4 w-10 h-10 bg-gray-800 dark:bg-gray-600 text-white rounded-full shadow-lg hover:shadow-xl hover:bg-gray-700 dark:hover:bg-gray-500 transition-all duration-300 flex items-center justify-center z-50"
onclick="window.scrollTo(0,0)"
aria-label="Scroll to top">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"></path>
</svg>
</button>

View file

@ -0,0 +1,119 @@
<nav class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
{% if logo.show_in_nav %}
{% if logo.has_dark_variant %}
<a href="/" class="flex-shrink-0 mr-4">
<img src="{{ logo.light_src }}" alt="{{ logo.alt }}" class="h-8 w-auto dark:hidden">
<img src="{{ logo.dark_src }}" alt="{{ logo.alt }}" class="h-8 w-auto hidden dark:block">
</a>
{% else %}
<a href="/" class="flex-shrink-0 mr-4">
<img src="{{ logo.light_src }}" alt="{{ logo.alt }}" class="h-8 w-auto">
</a>
{% endif %}
{% endif %}
<div class="hidden lg:flex lg:items-center md:ml-6 md:space-x-6">
{%- for item in menu_items -%}
{%- if "/acerca-de" in item.href or "/about" in item.href -%}
<details class="nav-dd">
<summary class="nav-dd__summary text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors">{{ item.label }}</summary>
<div class="nav-dd__menu">
<a href="{{ item.href }}" class="nav-dd__item">Ontoref</a>
{%- for it in menu_items %}{% if "/dominios" in it.href or "/domains" in it.href %}<a href="{{ it.href }}" class="nav-dd__item">{{ it.label }}</a>{% endif %}{% endfor -%}
{%- if "/acerca-de" in item.href %}<a href="/nosotros" class="nav-dd__item">Nosotros</a>{% else %}<a href="/about-us" class="nav-dd__item">About us</a>{% endif -%}
</div>
</details>
{%- elif "/dominios" in item.href or "/domains" in item.href -%}
{%- elif "/recetas" in item.href or "/recipes" in item.href -%}
<details class="nav-dd">
<summary class="nav-dd__summary text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors">{% if "/recetas" in item.href %}Más{% else %}More{% endif %}</summary>
<div class="nav-dd__menu">
<a href="{{ item.href }}" class="nav-dd__item">{{ item.label }}</a>
{%- for it in menu_items %}{% if "/recursos" in it.href or "/resources" in it.href %}<a href="{{ it.href }}" class="nav-dd__item">{{ it.label }}</a>{% endif %}{% endfor -%}
</div>
</details>
{%- elif "/recursos" in item.href or "/resources" in item.href -%}
{%- else -%}
<a href="{{ item.href }}"
class="text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors no-underline{% if item.is_active %} text-gray-900 dark:text-white font-semibold{% endif %}"
{% if item.is_external %}target="_blank" rel="noopener noreferrer"{% endif %}>{{ item.label }}</a>
{%- endif -%}
{%- endfor -%}
</div>
</div>
<div class="flex items-center space-x-2">
<div class="hidden md:flex md:items-center md:space-x-2">
<div class="flex items-center space-x-2 nav-ctl-group">
{{ theme_html | safe }}
{{ lang_html | safe }}
{{ login_html | safe }}
</div>
</div>
<div class="md:hidden flex items-center">
<button onclick="document.getElementById('navbar-collapse').classList.toggle('hidden')"
class="ds-mobile-menu-btn"
aria-controls="navbar-collapse"
aria-expanded="false"
aria-label="Open navigation menu">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
<div id="navbar-collapse" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="px-2 pt-2 pb-3 space-y-1">
{% for item in menu_items %}
<a href="{{ item.href }}"
class="block text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors no-underline{% if item.is_active %} text-gray-900 dark:text-white font-semibold{% endif %}"
{% if item.is_external %}target="_blank" rel="noopener noreferrer"{% endif %}>
{{ item.label }}
</a>
{% endfor %}
</div>
<div class="px-3 py-2 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500 dark:text-gray-400">{{ mobile_controls_label }}</span>
<div class="flex items-center space-x-2 nav-ctl-group">
{{ theme_html | safe }}
{{ lang_html | safe }}
{{ login_html | safe }}
</div>
</div>
</div>
</div>
</div>
<style>
.nav-dd{position:relative}
.nav-dd__summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center}
.nav-dd__summary::-webkit-details-marker{display:none}
.nav-dd__summary::after{content:"▾";margin-left:.3rem;font-size:.7em;opacity:.7}
.nav-dd[open]>.nav-dd__summary::after{transform:rotate(180deg)}
.nav-dd__menu{position:absolute;top:100%;left:0;margin-top:.35rem;min-width:12rem;background:#fff;border:1px solid rgba(0,0,0,.1);border-radius:.6rem;box-shadow:0 8px 24px rgba(0,0,0,.12);padding:.35rem;z-index:60}
.dark .nav-dd__menu{background:#1f2937;border-color:rgba(255,255,255,.1)}
.nav-dd__item{display:block;padding:.5rem .7rem;border-radius:.4rem;font-size:.875rem;color:#374151;text-decoration:none;white-space:nowrap;transition:background .12s}
.dark .nav-dd__item{color:#d1d5db}
.nav-dd__item:hover{background:rgba(0,0,0,.06)}
.dark .nav-dd__item:hover{background:rgba(255,255,255,.08)}
</style>
<script>
(function(){
if (window.__navDd) return; window.__navDd = true;
document.addEventListener('click', function(e){
document.querySelectorAll('details.nav-dd[open]').forEach(function(d){
if (!d.contains(e.target)) d.removeAttribute('open');
else if (e.target.closest('.nav-dd__item')) d.removeAttribute('open');
});
});
})();
</script>
</nav>

278
site/justfile Normal file
View file

@ -0,0 +1,278 @@
set shell := ["bash", "-c"]
set dotenv-load := true
project_url := "https://ontoref.dev"
project_root := justfile_directory()
source_project := justfile_directory() + "/../../../website-htmx-rustelo/code"
rustelo_root := justfile_directory() + "/../../../rustelo/code"
# `authoring` (not `content`) to avoid colliding with the flat content recipe below.
# Content-authoring toolkit via rustelo-content CLI: images | sync | validate | new
mod authoring ".just/content.just"
default:
@just --list
@echo -e "\nproject_url = {{ project_url }}"
# ── Server ──────────────────────────────────────────────────────────────────
# Start the HTMX-SSR dev server (auto-detects site/ in PWD)
[no-cd]
dev:
lsof -ti:3030 | xargs kill -9 2>/dev/null || true
rustelo-htmx-server
# ── Content ─────────────────────────────────────────────────────────────────
# Positional filters: `just content projects` or `just content projects es`.
# A `type=`/`lang=` prefix is tolerated, so `just content type=projects lang=es` works too.
# The targeted (--content-type/--language) path is non-incremental — it always rewrites.
#
# The two trees own DIFFERENT files, so the mirror runs in both directions:
# site/r ← content_processor writes the content indexes here (canonical);
# the runtime server reads it (SITE_SERVER_ROOT_CONTENT=r).
# site/public/r ← the nu generators write about/adr-map/taglines/content_graph here,
# and this is the tree the deploy pack ships (site/public/**).
# So: content indexes go r → public/r (or nothing new ever reaches production), and
# the four nu artifacts come back public/r → r (or dev serves June's about/adr-map).
# A whole-tree rsync in ONE direction cannot be right for both, and the old one
# (public/r → r) clobbered each fresh index with the stale deploy-tree copy.
# Regenerate content indexes from markdown in site/content/, then reconcile both trees.
[no-cd]
content type="" lang="":
#!/usr/bin/env bash
set -e
# Brand-linkify blog "ontoref" mentions → /about (idempotent, source-level).
nu "{{ project_root }}/scripts/build/linkify-site.nu" "{{ project_root }}/site/content/blog/**/*.md"
# just args are POSITIONAL: `just content type=projects` binds type="type=projects".
# Strip an optional `key=` prefix so both positional and key=value forms work.
TYPE="{{ type }}"; TYPE="${TYPE#*=}"
LANG_ARG="{{ lang }}"; LANG_ARG="${LANG_ARG#*=}"
ARGS=()
[ -n "$TYPE" ] && ARGS+=(--content-type "$TYPE")
[ -n "$LANG_ARG" ] && ARGS+=(--language "$LANG_ARG")
content_processor "${ARGS[@]}"
# Content indexes → deploy tree. --delete is what keeps a renamed slug from living
# on as a live URL: content_processor prunes site/r, and without --delete the dead
# page would survive in public/r and ship to production. The four nu artifacts are
# excluded (site/r holds only stale copies that would overwrite the fresh originals
# gen-adr-map / gen-about-pages / gen-taglines just wrote) — and --exclude also
# protects them from --delete, since they legitimately exist only on this side.
rsync -a --delete \
--exclude about.json --exclude adr-map.json \
--exclude taglines.json --exclude content_graph.json \
"{{ project_root }}/site/r/" "{{ project_root }}/site/public/r/"
# nu artifacts → serve tree, so `./run.sh` renders the current About/ADR map.
for f in about.json adr-map.json taglines.json content_graph.json; do
src="{{ project_root }}/site/public/r/$f"
if [ -f "$src" ]; then cp -f "$src" "{{ project_root }}/site/r/$f"; fi
done
# Watch mode: auto-regenerate content on file changes
[no-cd]
content-watch type="" lang="":
#!/usr/bin/env bash
set -e
TYPE="{{ type }}"; TYPE="${TYPE#*=}"
LANG_ARG="{{ lang }}"; LANG_ARG="${LANG_ARG#*=}"
ARGS=(--watch)
[ -n "$TYPE" ] && ARGS+=(--content-type "$TYPE")
[ -n "$LANG_ARG" ] && ARGS+=(--language "$LANG_ARG")
content_processor "${ARGS[@]}"
# Regenerate content_graph.json (mini-graphs + ONTOLOGY CONTEXT sidebar)
[no-cd]
graph daemon=project_url:
RUSTELO_ROOT="{{ rustelo_root }}" \
ONTOREF_NICKEL_IMPORT_PATH="{{ project_root }}/../../code/ontology" \
CONTENT_GRAPH_IMPL_ONTOLOGY="{{ project_root }}/../../.ontoref/ontology/core.ncl" \
CONTENT_GRAPH_IMPL_ADRS_GLOB="{{ project_root }}/../../.ontoref/adrs/adr-[0-9]*.ncl" \
CONTENT_GRAPH_FRAMEWORK_ONTOLOGY="{{ rustelo_root }}/../.ontoref/ontology/core.ncl" \
CONTENT_GRAPH_FRAMEWORK_ADRS_GLOB="{{ rustelo_root }}/../.ontoref/adrs/adr-[0-9]*.ncl" \
CONTENT_GRAPH_DAEMON_BASE="{{ daemon }}" \
gen-content-graph
# Regenerate ADR reference map from .ontoref/adrs → site/public/r/adr-map.json
[no-cd]
adr-map daemon=project_url:
nu "{{ project_root }}/scripts/build/gen-adr-map.nu" --root "{{ project_root }}/../.." --daemon "{{ daemon }}"
# The web-usable form of `onre diagram` (served at /images/, the static asset root).
# Referenced by the About graph block, so must run before about-json.
# Project the ontology (core.ncl) → site/public/images/ontoref-diagram.svg.
[no-cd]
diagram:
nu "{{ project_root }}/scripts/build/gen-diagram.nu" --root "{{ project_root }}/../.." --out "{{ project_root }}/site/public/images/ontoref-diagram.svg"
nu "{{ project_root }}/scripts/build/gen-graph-page.nu" --root "{{ project_root }}/../.." --out "{{ project_root }}/site/public/images/ontoref-graph.html"
# Makes /catalog (browse) + /catalog/{slug} serve as SSR content (runtime kind, no
# rebuild — registered in site/content/content-kinds.toml).
# Project .ontoref/catalog → catalog markdown, then build the content index.
[no-cd]
catalog-content:
nu "{{ project_root }}/scripts/build/gen-catalog-pages.nu" --root "{{ project_root }}/../.." --mode content --content-out "{{ project_root }}/site/content/catalog/en"
content_processor --content-type catalog
# Hot-reloaded by the SSR /about handler (via pages/about_project.j2 when kind=project).
# daemon="" → the "see it live" graph link is emitted host-relative (/graph?…),
# because on the SSR site /graph is served by this same origin. The static
# outreach/web pages pass an absolute --daemon since they may be served elsewhere.
# Emit the level-aware About view-model → site/public/r/about.json (needs adr-map fresh).
[no-cd]
about-json daemon="": diagram
nu "{{ project_root }}/scripts/build/gen-about-pages.nu" \
--root "{{ project_root }}/../.." \
--emit-json "{{ project_root }}/site/public/r/about.json" \
--daemon "{{ daemon }}"
# ── Spine (bl-035) ────────────────────────────────────────────────────────────
# Generate spine landing pages from .ontoref/positioning/spine.ncl
[no-cd]
spine daemon=project_url:
nu "{{ project_root }}/scripts/build/gen-spine-pages.nu" \
--root "{{ project_root }}/../.." \
--content "{{ project_root }}/site/content" \
--daemon "{{ daemon }}"
# ── Taglines (rotating hero phrases) ──────────────────────────────────────────
# Drift-checked at export (taglines-schema.ncl); the hero rotator fetches /r/taglines.json.
# Project .ontoref/positioning/taglines.ncl → site/public/r/taglines.json.
[no-cd]
taglines:
nu "{{ project_root }}/scripts/build/gen-taglines.nu" \
--root "{{ project_root }}/../.." \
--out "{{ project_root }}/site/public/r/taglines.json"
# Projects code/Cargo.toml [workspace.package].version into the home hero badge — the
# one hand-written version literal on the site. Run after a version bump so the badge
# never drifts. Scoped to the home-hero-badge line, so other version strings are untouched.
# Stamp the canonical workspace version into the home hero badge (en + es).
[no-cd]
stamp-version:
#!/usr/bin/env nu
let ver = (open "{{ project_root }}/../../code/Cargo.toml" | get workspace.package.version)
let ftls = [
"{{ project_root }}/site/i18n/locales/en/pages/home.ftl"
"{{ project_root }}/site/i18n/locales/es/pages/home.ftl"
]
for f in $ftls {
let out = (open --raw $f | lines | each {|l|
if ($l | str starts-with "home-hero-badge") {
$l | str replace --all --regex 'v\d+\.\d+\.\d+' $"v($ver)"
} else { $l }
} | str join "\n")
$"($out)\n" | save --force $f
}
print $"stamped v($ver) into home-hero-badge \(en + es\)"
# From the constellation canonical source (assets/vendor) into the site asset trees
# (_public source + public served mirror).
# Materialize the shared hero rotator + typed.js vendor into the site asset trees.
[no-cd]
rotator-assets:
cp -f "{{ project_root }}/../../assets/vendor/tagline-rotator.js" "{{ project_root }}/site/_public/js/tagline-rotator.js"
cp -f "{{ project_root }}/../../assets/vendor/typed.umd.js" "{{ project_root }}/site/_public/js/typed.umd.js"
cp -f "{{ project_root }}/../../assets/vendor/tagline-rotator.js" "{{ project_root }}/site/public/js/tagline-rotator.js"
cp -f "{{ project_root }}/../../assets/vendor/typed.umd.js" "{{ project_root }}/site/public/js/typed.umd.js"
# assets/site/ is the source of truth; only the web formats are mirrored — PNG/JPG
# originals stay out of the deploy. Safe to re-run.
# Mirror web-optimised images (AVIF + WebP) from assets/site/ → site/public/images/.
[no-cd]
sync-site-images:
#!/usr/bin/env nu
let src = "{{ project_root }}/../../assets/site"
let dest = "{{ project_root }}/site/public/images"
mkdir $dest
let files = (glob $"($src)/*.{avif,webp}")
if ($files | is-empty) {
error make { msg: $"no AVIF/WebP found in ($src)" }
}
$files | each {|f| cp $f $dest }
print $"synced ($files | length) image\(s\) from ($src) → ($dest)"
# ── Templates ───────────────────────────────────────────────────────────────
# Safe to re-run; overwrites htmx-templates/ from scratch — which is exactly why the
# site's own templates cannot live in the output. The assembler has two layers
# (framework defaults, then source-project templates) but the level chain has three:
# rustelo → website-htmx-rustelo → outreach/site. site/templates-overlay/ is the
# missing third slot: the only declared home for template work owned by THIS level.
# Anything edited straight into htmx-templates/ is destroyed on the next run — that is
# how the nav submenus were lost once already.
# Assemble htmx-templates: framework defaults → source-project templates → site overlay.
[no-cd]
templates:
nu "{{ source_project }}/scripts/build/assemble-htmx-templates.nu" \
--project-templates "{{ source_project }}/crates/pages_htmx/templates" \
--out "{{ project_root }}/htmx-templates"
cp -R "{{ project_root }}/site/templates-overlay/." "{{ project_root }}/htmx-templates/"
# Proves `just templates` is lossless: reassemble into a temp tree and diff it against
# the live one. Any difference means htmx-templates/ holds work that exists nowhere
# else and the next `just templates` (or `just sync`, which calls it) will delete it —
# the fix is to move that work into site/templates-overlay/, never to skip the run.
# Gate: assert htmx-templates/ is exactly reproducible from its declared sources.
[no-cd]
templates-check:
#!/usr/bin/env bash
set -e
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
nu "{{ source_project }}/scripts/build/assemble-htmx-templates.nu" \
--project-templates "{{ source_project }}/crates/pages_htmx/templates" \
--out "$tmp" >/dev/null
cp -R "{{ project_root }}/site/templates-overlay/." "$tmp/"
if diff -r "{{ project_root }}/htmx-templates" "$tmp" >/dev/null 2>&1; then
echo "templates-check: htmx-templates/ is reproducible — no unwitnessed work"
else
echo "templates-check: DRIFT — htmx-templates/ holds work absent from its sources:"
diff -rq "{{ project_root }}/htmx-templates" "$tmp" | sed "s|$tmp|<regenerated>|g"
exit 1
fi
# Safe to re-run; overwrites htmx-assets/ from scratch.
# Materialize the framework htmx runtime (htmx.min.js + ext/*) into a top-level
# htmx-assets/ tree (sibling of htmx-templates/). Shipped to the PV by content.nu
# (distro.ncl deliverable) and served at /assets/htmx/* via HTMX_ASSETS_PATH — the
# runtime lives OUTSIDE site/public (router mounts /assets/htmx as its own ServeDir,
# not from the public tree). Dev masks this via the local-install wrapper's
# HTMX_ASSETS_PATH; prod has no such mount. Source of truth: templates/shared/htmx.
[no-cd]
htmx-assets:
rm -rf "{{ project_root }}/htmx-assets"
mkdir -p "{{ project_root }}/htmx-assets/ext"
cp -f "{{ rustelo_root }}/templates/shared/htmx/htmx.min.js" "{{ project_root }}/htmx-assets/htmx.min.js"
cp -R "{{ rustelo_root }}/templates/shared/htmx/ext/." "{{ project_root }}/htmx-assets/ext/"
# ── Posts ─────────────────────────────────────────────────────────────────────
# Brand-linkify mentions → /about, regenerate the content indexes, and rebuild the
# content graph (mini-graph + sidecar relations). Run after editing a post + .ncl sidecar.
# Build a post for publication: linkify + content indexes + content graph.
[no-cd]
post: content graph
# 1. every brand mention is already linkified (no `just content` pending)
# 2. every post graph sidecar resolves into content_graph.json (no stale graph)
# Verify ALL posts are publication-ready without mutating anything (CI gate).
[no-cd]
posts-check:
#!/usr/bin/env bash
set -e
nu "{{ project_root }}/scripts/build/linkify-site.nu" --check "{{ project_root }}/site/content/blog/**/*.md"
nu "{{ project_root }}/scripts/build/check-graph-coverage.nu" "{{ project_root }}"
# ── Sync ────────────────────────────────────────────────────────────────────
# Full resync: templates + htmx runtime + vendor assets + content indexes
# (no graph — that needs ONTOREF env)
[no-cd]
sync: templates htmx-assets rotator-assets content
# Requires ONTOREF_NICKEL_IMPORT_PATH in .env.
# Full resync including content_graph + ADR map + About view-model.
[no-cd]
sync-full: templates htmx-assets rotator-assets content catalog-content graph adr-map about-json

20
site/run-secure.sh Executable file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Standalone secure launch: decrypt the SOPS secrets in-memory and inject them
# as ENV for the server (nothing plaintext hits disk). email.ncl reads
# ${SMTP_*}/${EMAIL_FROM} from that injected env.
#
# ./run-secure.sh
#
# Secrets file + age identity are overridable:
# SECRETS=/path/to/x.sops.env SOPS_AGE_KEY_FILE=/path/to/id.kage ./run-secure.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
SECRETS="${SECRETS:-$SCRIPT_DIR/../../vault/ontoref-site.sops.env}"
# Default to the ontoref vault identity; in infra, SOPS_AGE_KEY_FILE points at
# the libre-wuji key (also a recipient of the blob).
export SOPS_AGE_KEY_FILE="${SOPS_AGE_KEY_FILE:-$SCRIPT_DIR/../../vault/ontoref.kage}"
exec sops exec-env "$SECRETS" ./run.sh

42
site/run.sh Executable file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
lsof -ti:3030 | xargs kill -9 2>/dev/null || true
# Secrets (SMTP creds, CRYPTO_KEY, …) are provided as ENV by the deploy/provisioning
# layer (SOPS-decrypted at run time — e.g. `sops exec-env`), not from a plaintext
# file here. email.ncl reads ${SMTP_*}/${EMAIL_FROM} from that injected env.
# Only export what the server genuinely needs from here. NICKEL_IMPORT_PATH and
# SITE_CONFIG_PATH are intentionally LEFT to the binary's built-in defaults
# (which resolve site/contracts.ncl from ~/.local/share/rustelo/nickel) —
# exporting a relative path here broke NCL contract resolution.
export DATABASE_URL="${DATABASE_URL:-sqlite:data/dev.db}"
# Templates from the local runtime tree so .j2 edits need no cargo rebuild.
export HTMX_TEMPLATE_PATH="${HTMX_TEMPLATE_PATH:-htmx-templates}"
# Canonical public origin — feeds SEO canonical/OG URLs (seo::site_base_url).
# Without it the head falls back to http://127.0.0.1:3030.
export SITE_BASE_URL="${SITE_BASE_URL:-https://ontoref.dev}"
export SITE_NAME="${SITE_NAME:-ontoref}"
# Logo is config-driven: site/config/site.ncl [logo]. No env needed.
# ── Email delivery (post comments + contact form) ──────────────────────────
# Owner inbox for new post comments and contact-form submissions. Change to your
# real address. Delivery also needs email.enabled=true in site/config/email.ncl
# (provider=console logs to stdout in dev; set provider=smtp + creds for real send).
export POST_MESSAGE_RECIPIENT="${POST_MESSAGE_RECIPIENT:-jpl@jesusperez.pro}"
export CONTACT_RECIPIENT="${CONTACT_RECIPIENT:-$POST_MESSAGE_RECIPIENT}"
export EMAIL_FROM="${EMAIL_FROM:-noreply@ontoref.dev}"
export EMAIL_FROM_NAME="${EMAIL_FROM_NAME:-$SITE_NAME}"
# Quiet noisy framework tracing (these are informational logs the framework emits at
# WARN, not real problems): per-request routing conversions (🔄/🔍/✅), the category
# filter-index fallback ("using empty meta config"), NATS-not-configured, and the
# missing rustelo/migrations dir. Genuine warnings/errors (incl. RBAC audit) still show.
# Override by exporting RUST_LOG yourself before running.
export RUST_LOG="${RUST_LOG:-warn,rustelo_core_lib::routing=error,rustelo_core_lib::categories::cache=error,rustelo_server::nats=error,rustelo_server::migrations=error}"
exec rustelo-htmx-server

View file

@ -0,0 +1,44 @@
# Rustelo Manifest — Single Source of Truth for Path Resolution
# PAP-Compliant: configuration-driven, no hardcoding, language-agnostic
[manifest]
version = "1.0"
schema = "https://rustelo.dev/schemas/manifest/v1"
[project]
name = "ontoref-website"
type = "rustelo-app"
description = "Bilingual EN/ES website built on the Rustelo framework"
[paths]
# All paths relative to manifest location
content = "site/public/r"
config = "site/config"
routes = "site/config/routes"
i18n = "site/i18n"
assets = "site/public"
ui = "site/config"
build_output = "site/public"
wasm_output = "site/public/pkg"
[discovery]
default_lang = "en"
languages = "auto"
content_types = "auto"
[deployment]
namespace = "ontoref"
cp_node = "libre-wuji-cp-0"
base_url = "${BASE_URL:-http://localhost:3030}"
content_url = "/r"
content_root = "r"
cache_path = "rustelo-cache"
api_endpoint = "${API_ENDPOINT:-/api}"
database_url = "${DATABASE_URL:-sqlite:data/dev.db}"
[build]
leptos_output_name = "website"
site_addr = "127.0.0.1:3030"
reload_port = 3031
debug = 0
cache_build_path = "rustelo-cache"

View file

@ -0,0 +1,35 @@
-- Post engagement surface: unique views, 1-5 ratings, and post messages.
-- Applied by rustelo_server::migrations::MigrationRunner on PostgreSQL connect.
-- Identity is an opaque visitor_id (first-party `vid` cookie); no raw IP/UA is stored.
CREATE TABLE IF NOT EXISTS post_view (
post_id TEXT NOT NULL,
lang TEXT NOT NULL,
visitor_id TEXT NOT NULL,
first_seen TIMESTAMPTZ NOT NULL,
PRIMARY KEY (post_id, lang, visitor_id)
);
CREATE TABLE IF NOT EXISTS post_rating (
post_id TEXT NOT NULL,
visitor_id TEXT NOT NULL,
value INTEGER NOT NULL CHECK (value BETWEEN 1 AND 5),
updated_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (post_id, visitor_id)
);
CREATE TABLE IF NOT EXISTS post_message (
id TEXT PRIMARY KEY,
post_id TEXT NOT NULL,
post_title TEXT NOT NULL,
lang TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT,
body TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'new', -- new | approved | spam (approved => public later)
visitor_id TEXT,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_post_view_post ON post_view(post_id, lang);
CREATE INDEX IF NOT EXISTS idx_post_message_post ON post_message(post_id, status);

View file

@ -0,0 +1,35 @@
-- Post engagement surface: unique views, 1-5 ratings, and post messages.
-- Applied by rustelo_server::migrations::MigrationRunner on SQLite connect.
-- Identity is an opaque visitor_id (first-party `vid` cookie); no raw IP/UA is stored.
CREATE TABLE IF NOT EXISTS post_view (
post_id TEXT NOT NULL,
lang TEXT NOT NULL,
visitor_id TEXT NOT NULL,
first_seen TEXT NOT NULL,
PRIMARY KEY (post_id, lang, visitor_id)
);
CREATE TABLE IF NOT EXISTS post_rating (
post_id TEXT NOT NULL,
visitor_id TEXT NOT NULL,
value INTEGER NOT NULL CHECK (value BETWEEN 1 AND 5),
updated_at TEXT NOT NULL,
PRIMARY KEY (post_id, visitor_id)
);
CREATE TABLE IF NOT EXISTS post_message (
id TEXT PRIMARY KEY,
post_id TEXT NOT NULL,
post_title TEXT NOT NULL,
lang TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT,
body TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'new', -- new | approved | spam (approved => public later)
visitor_id TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_post_view_post ON post_view(post_id, lang);
CREATE INDEX IF NOT EXISTS idx_post_message_post ON post_message(post_id, status);

View file

@ -0,0 +1,5 @@
-- Publish-consent for post messages: whether the visitor authorised showing
-- their comment publicly, plus timestamp + IP recorded as proof of consent.
ALTER TABLE post_message ADD COLUMN IF NOT EXISTS publish_consent BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE post_message ADD COLUMN IF NOT EXISTS consent_at TIMESTAMPTZ;
ALTER TABLE post_message ADD COLUMN IF NOT EXISTS consent_ip TEXT;

View file

@ -0,0 +1,5 @@
-- Publish-consent for post messages: whether the visitor authorised showing
-- their comment publicly, plus timestamp + IP recorded as proof of consent.
ALTER TABLE post_message ADD COLUMN publish_consent INTEGER NOT NULL DEFAULT 0;
ALTER TABLE post_message ADD COLUMN consent_at TEXT;
ALTER TABLE post_message ADD COLUMN consent_ip TEXT;

View file

@ -0,0 +1,38 @@
#!/usr/bin/env nu
# Verify every blog post that ships a graph sidecar (.ncl) is present in the
# generated content graph (site/public/r/content_graph.json). A sidecar with no
# entry means `just graph` was never re-run after the post was added — the post
# would render without its mini-graph and ONTOLOGY CONTEXT relations.
#
# Read-only. Exits non-zero on any gap, so it works as a CI gate.
#
# Usage (from outreach/site):
# nu scripts/build/check-graph-coverage.nu .
def main [root: string = "."] {
let graph_path = ($root | path join "site/public/r/content_graph.json")
if not ($graph_path | path exists) {
error make { msg: $"content_graph.json not found at ($graph_path) — run `just graph`" }
}
let keys = (open $graph_path | get mini | columns)
let sidecars = (glob $"($root)/site/content/blog/**/*.ncl" | where { |p| ($p | path basename) != "_index.ncl" })
mut missing = 0
for s in $sidecars {
let id = (open --raw $s | parse --regex 'id\s*=\s*"(?<id>[^"]+)"' | get id.0?)
if ($id == null) {
print $" ($s | path basename): no id field"
$missing = ($missing + 1)
} else if ($id not-in $keys) {
print $" MISSING in content graph: ($id) \(($s | path basename)\)"
$missing = ($missing + 1)
}
}
if $missing > 0 {
error make { msg: $"graph-coverage: ($missing) post sidecar\(s\) absent from content_graph.json — run `just graph`" }
}
print $"graph-coverage: ($sidecars | length) sidecar\(s\) present in content graph"
}

View file

@ -0,0 +1,382 @@
#!/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"
}
}

View file

@ -0,0 +1,43 @@
#!/usr/bin/env nu
# Project the ADR set into a flat reference map — the single source consumed by
# both outreach surfaces: the site sidebar (merged into content_graph.json) and
# the outreach/web linkifier. Source of truth: .ontoref/adrs/adr-NNN.ncl.
#
# Two-tier links (ADR-057 hook-static-prize-live): the map carries the ADR's
# human title (the daemon-free static label) plus the live deep-links —
# detail_url → daemon ADR page, graph_url → daemon graph focused on the node.
#
# Usage:
# nu scripts/build/gen-adr-map.nu --root <constellation-root> [--daemon <url>] [--out <path>]
def main [
--root: string = "../.." # constellation root, relative to outreach/site
--daemon: string = "https://ontoref.dev" # daemon base the deep-links resolve against
--out: string = "site/public/r/adr-map.json" # output path, relative to cwd (outreach/site)
] {
let adrs_dir = $"($root)/.ontoref/adrs"
let ip_data = $"($root)/code/ontology"
let files = (
glob $"($adrs_dir)/adr-*.ncl"
| where {|f| ($f | path basename) =~ '^adr-[0-9]' }
| sort
)
mut map = {}
for f in $files {
let adr = (nickel export --format json --import-path $ip_data --import-path $adrs_dir $f | from json)
let id = $adr.id
let slug = ($id | str replace 'adr-' '')
$map = ($map | insert $id {
name: $adr.title,
status: ($adr.status? | default "Unknown"),
detail_url: $"($daemon)/adr/($slug)",
graph_url: $"($daemon)/graph?focus=($id)",
})
}
$map | to json --indent 2 | save -f $out
print $"adr-map: ($map | columns | length) ADRs → ($out)"
}

View file

@ -0,0 +1,262 @@
#!/usr/bin/env nu
# Project the ADR set (.ontoref/adrs) into renderable content — same role as
# gen-spine-pages / gen-content-graph, decoupled from the live ADRs. Two modes:
# content : markdown per ADR into the `adr` content-kind tree
# static : self-contained brand-wrapped HTML viewer — outreach/web + docs
#
# Page chrome (brand palette, dark/light theme, nav, .doc-* CSS) comes from the
# shared lib/doc-page.nu module so ADR + catalog surfaces look identical.
#
# Usage:
# nu scripts/build/gen-adr-pages.nu --root ../.. --mode both --slug ontoref
use lib/doc-page.nu [esc paras sec page-shell]
def short-adr [rid: string] {
let m = ($rid | parse --regex '(?P<k>adr-\d+)')
if ($m | is-empty) { $rid } else { $m.k.0 }
}
# ── section renderers (HTML, static mode) ─────────────────────────────────────
def html-constraints [items: list] {
if ($items | is-empty) { return "" }
let rows = ($items | each {|c|
let sev = ($c.severity? | default "")
$"<li><span class=\"doc-badge doc-sev-(($sev | str downcase))\">(if ($sev | is-empty) { '—' } else { esc $sev })</span><div><p>(esc ($c.claim? | default ''))</p></div></li>"
} | str join)
$"<ul class=\"doc-list\">($rows)</ul>"
}
def html-alts [items: list] {
if ($items | is-empty) { return "" }
let rows = ($items | each {|a|
$"<li><p class=\"doc-alt-opt\">(esc ($a.option? | default ''))</p><p class=\"doc-alt-why\"><strong>Rejected:</strong> (esc ($a.why_rejected? | default ''))</p></li>"
} | str join)
$"<ul class=\"doc-list\">($rows)</ul>"
}
def html-anti [items: list] {
if ($items | is-empty) { return "" }
let rows = ($items | each {|p|
if ($p | describe | str starts-with "record") {
$"<li><strong>(esc ($p.name? | default ($p.id? | default '')))</strong><p>(esc ($p.description? | default ''))</p></li>"
} else { $"<li>(esc $p)</li>" }
} | str join)
$"<ul class=\"doc-list\">($rows)</ul>"
}
# Related ADR links: /adr/NNN path, ADR-NNN display text
def html-related [ids: list] {
if ($ids | is-empty) { return "" }
let links = ($ids | each {|r|
let k = (short-adr $r)
let num = ($k | str replace 'adr-' '')
$"<a href=\"/adr/($num)\">ADR-(esc $num)</a>"
} | str join " · ")
$"<p class=\"doc-related\">($links)</p>"
}
def html-toc [adr: record] {
let secs = [
[anchor title present];
[context "Context" ($adr.context? | default "" | is-not-empty)]
[decision "Decision" ($adr.decision? | default "" | is-not-empty)]
[constraints "Constraints" (($adr.constraints? | default []) | is-not-empty)]
[alternatives "Alternatives" (($adr.alternatives_considered? | default []) | is-not-empty)]
[anti-patterns "Anti-patterns" (($adr.anti_patterns? | default []) | is-not-empty)]
[related "Related ADRs" (($adr.related_adrs? | default []) | is-not-empty)]
]
let items = ($secs | where present | each {|s| $"<li><a href=\"#($s.anchor)\">(esc $s.title)</a></li>" } | str join)
if ($items | is-empty) { "" } else { $"<nav class=\"doc-toc\"><p>On this page</p><ul>($items)</ul></nav>" }
}
# ── markdown render (content-kind mode) ───────────────────────────────────────
def render-md [adr: record, daemon: string, slug: string] {
let aid = $adr.id
let num = ($aid | str replace 'adr-' '')
let status = ($adr.status? | default "Unknown")
let st_l = ($status | str downcase)
let title = ($adr.title? | default "" | str replace --all '"' "'")
let excerpt = ($adr.context? | default "" | str trim | split row "\n" | first | default "" | str substring 0..200 | str replace --all '"' "'")
let front = $"---
id: \"($aid)\"
title: \"($title)\"
slug: \"($num)\"
subtitle: \"($status)\"
excerpt: \"($excerpt)\"
author: \"ontoref\"
date: \"($adr.date? | default '')\"
published: true
featured: false
category: \"($st_l)\"
tags: [\"($aid)\", \"adr\", \"($st_l)\"]
css_class: \"category-adr\"
---
"
let cons = (($adr.constraints? | default []) | each {|c| $"<li><strong>(esc ($c.severity? | default ''))</strong> (esc ($c.claim? | default ''))</li>" } | str join)
let alts = (($adr.alternatives_considered? | default []) | each {|a| $"<li><strong>(esc ($a.option? | default ''))</strong> — <em>rejected:</em> (esc ($a.why_rejected? | default ''))</li>" } | str join)
let anti = (($adr.anti_patterns? | default []) | each {|p|
if ($p | describe | str starts-with "record") { $"<li><strong>(esc ($p.name? | default ($p.id? | default '')))</strong> — (esc ($p.description? | default ''))</li>" } else { $"<li>(esc $p)</li>" }
} | str join)
let rel = (($adr.related_adrs? | default []) | each {|r| let k = (short-adr $r); let rn = ($k | str replace 'adr-' ''); $"<a href=\"/adr/($rn)\">ADR-(esc $rn)</a>" } | str join " · ")
mut body = $"<h2>Context</h2>\n\n(paras ($adr.context? | default ''))\n\n<h2>Decision</h2>\n\n(paras ($adr.decision? | default ''))\n"
if ($cons | is-not-empty) { $body = $body + $"\n<h2>Constraints</h2>\n\n<ul>($cons)</ul>\n" }
if ($alts | is-not-empty) { $body = $body + $"\n<h2>Alternatives considered</h2>\n\n<ul>($alts)</ul>\n" }
if ($anti | is-not-empty) { $body = $body + $"\n<h2>Anti-patterns</h2>\n\n<ul>($anti)</ul>\n" }
if ($rel | is-not-empty) { $body = $body + $"\n<h2>Related ADRs</h2>\n\n<p>($rel)</p>\n" }
$"($front)($body)"
}
# Sidecar .ncl for the content-graph generator (foot mini-graph)
def render-adr-ncl [adr: record] {
let aid = $adr.id
let num = ($aid | str replace 'adr-' '')
let st_l = ($adr.status? | default "" | str downcase)
let title = ($adr.title? | default "" | str replace --all '"' "'")
let rel = (($adr.related_adrs? | default []) | each {|r| short-adr $r } | uniq | where {|k| $k != $aid } | each {|k| $"\"($k)\"" } | str join ", ")
$"{
id = \"($aid)\",
title = \"($title)\",
slug = \"($num)\",
excerpt = \"\",
tags = [\"adr\", \"($st_l)\"],
page_route = \"/adr/($num)\",
graph = {
implements = [],
related_to = [($rel)],
},
}
"
}
# ── page render (static mode) ─────────────────────────────────────────────────
def render-html [adr: record, daemon: string, slug: string, mini_svg: string] {
let aid = $adr.id
let status = ($adr.status? | default "Unknown")
let st_l = ($status | str downcase)
let graph_section = if ($mini_svg | is-not-empty) {
$"<div class=\"doc-mini-graph\">($mini_svg)</div>"
} else { "" }
let body = ([
$"<div class=\"doc-head\">
<span class=\"doc-badge doc-st-($st_l)\">(esc $status)</span>
<span class=\"doc-meta\">(esc $aid) · (esc ($adr.date? | default ''))</span>
<h1 style=\"margin:.5rem 0 0\">(esc ($adr.title? | default ''))</h1>
</div>"
$graph_section
(html-toc $adr)
(sec "context" "Context" (paras ($adr.context? | default "")))
(sec "decision" "Decision" (paras ($adr.decision? | default "")))
(sec "constraints" "Constraints" (html-constraints ($adr.constraints? | default [])))
(sec "alternatives" "Alternatives considered" (html-alts ($adr.alternatives_considered? | default [])))
(sec "anti-patterns" "Anti-patterns" (html-anti ($adr.anti_patterns? | default [])))
(sec "related" "Related ADRs" (html-related ($adr.related_adrs? | default [])))
] | str join "\n")
page-shell ($adr.title? | default "") $body
}
def render-index [adrs: list] {
let order = ["Accepted" "Proposed" "Superseded" "Deprecated"]
let groups = ($adrs | group-by {|a| $a.status? | default "Other" })
let keys = ($order | append ($groups | columns) | uniq | where {|k| $k in ($groups | columns) })
let search_entries = ($adrs | each {|a|
let id = ($a.id? | default "")
let num = ($id | str replace 'adr-' '')
let title = ($a.title? | default "" | str replace --all "'" "\\'")
let status = ($a.status? | default "" | str replace --all "'" "\\'")
let ctx = ($a.context? | default "" | str trim | str substring 0..500 | str replace --all "'" "\\'" | str replace --all "\n" " ")
let dec = ($a.decision? | default "" | str trim | str substring 0..500 | str replace --all "'" "\\'" | str replace --all "\n" " ")
$" \{id:'($id)',num:'($num)',title:'($title)',status:'($status)',text:'($ctx) ($dec)'\}"
} | str join ",\n")
let blocks = ($keys | each {|st|
let items = ($groups | get $st | sort-by id)
let rows = ($items | each {|a|
let num = ($a.id | str replace 'adr-' '')
$"<li data-id=\"($a.id)\" data-title=\"(esc ($a.title? | default ''))\" data-text=\"(esc ($a.context? | default '' | str trim | str substring 0..300)) (esc ($a.decision? | default '' | str trim | str substring 0..300))\"><a href=\"/adr/($num)/\"><code>ADR-(esc $num)</code>(esc ($a.title? | default ''))</a></li>"
} | str join)
$"<div class=\"doc-idx-group\" data-status=\"(esc $st)\"><h2>(esc $st) \(($items | length)\)</h2><ul class=\"doc-idx-list\">($rows)</ul></div>"
} | str join)
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 1rem\">Architecture Decision Records</h1>
<input id=\"doc-search\" class=\"doc-search\" type=\"search\" placeholder=\"Search ADRs by title or content…\">
<p id=\"no-results\" class=\"no-results\">No ADRs match your search.</p>
($blocks)
($search_js)"
page-shell "Architecture Decision Records" $content
}
# ── main ──────────────────────────────────────────────────────────────────────
def main [
--root: string = "../.."
--mode: string = "static" # static | content | both
--out: string = "site/public/adr" # static output dir
--content-out: string = "site/content/adr/en" # content-kind output
--daemon: string = "https://ontoref.dev"
--slug: string = "ontoref"
] {
let adrs_dir = $"($root)/.ontoref/adrs"
let ip_data = $"($root)/code/ontology"
let daemon = ($daemon | str trim --right --char '/')
let do_static = ($mode in ["static" "both"])
let do_content = ($mode in ["content" "both"])
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 files = (glob $"($adrs_dir)/adr-*.ncl" | where {|f| ($f | path basename) =~ '^adr-[0-9]' } | sort)
mut adrs = []
for f in $files {
let adr = (try { nickel export --format json --import-path $ip_data --import-path $adrs_dir $f | from json } catch { null })
if ($adr == null) { print $" skip ($f | path basename)"; continue }
$adrs = ($adrs | append $adr)
if $do_static {
let num = ($adr.id | str replace 'adr-' '')
let dir = $"($out)/($num)"
mkdir $dir
let mini_svg = (if $num in $mini_graphs { $mini_graphs | get $num } else { "" })
render-html $adr $daemon $slug $mini_svg | save -f $"($dir)/index.html"
}
if $do_content {
let cat = ($adr.status? | default "adr" | str downcase)
mkdir $"($content_out)/($cat)"
render-md $adr $daemon $slug | save -f $"($content_out)/($cat)/($adr.id).md"
render-adr-ncl $adr | save -f $"($content_out)/($cat)/($adr.id).ncl"
}
}
if $do_static {
mkdir $out
render-index $adrs | save -f $"($out)/index.html"
}
print $"gen-adr-pages [($mode)]: ($adrs | length) ADRs"
}

View file

@ -0,0 +1,313 @@
#!/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"
}

View file

@ -0,0 +1,124 @@
#!/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 '&' '&amp;' | str replace --all '<' '&lt;' | str replace --all '>' '&gt;'
}
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)"
}

View file

@ -0,0 +1,150 @@
#!/usr/bin/env nu
# Project the ontology (core.ncl) into a SELF-CONTAINED, navigable Cytoscape graph
# page. This is the interactive "prize" the About graph block links to (ADR-057
# hook-static / prize-live) — but daemon-free: the data is embedded and the page
# uses the cytoscape libs already shipped under /js/. The site's own /graph
# (Leptos GraphView) renders an empty canvas under htmx-ssr (no WASM hydration),
# so we ship our own page instead.
#
# Served as a static file at /images/ontoref-graph.html (only /images, /js, /styles
# are static-served; bare paths are intercepted by the content router).
#
# Usage (from outreach/site):
# nu scripts/build/gen-graph-page.nu --root ../.. --out site/public/images/ontoref-graph.html
const TEMPLATE = r##'<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ontoref · ontology graph</title>
<style>
:root{--amber:#E8A838;--silver:#C0CCD8;--dark:#0F1319;--surface:#141c24;--border:#1e2a38;--gray:#8090A4}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--dark);color:var(--silver);font-family:Inter,system-ui,sans-serif}
#bar{position:fixed;top:0;left:0;right:0;height:52px;display:flex;align-items:center;gap:14px;padding:0 18px;border-bottom:1px solid var(--border);background:rgba(15,19,25,.86);backdrop-filter:blur(8px);z-index:5}
#bar .brand{font-family:ui-monospace,monospace;font-weight:700;color:var(--amber);font-size:1.05rem;text-decoration:none}
#bar .brand:hover{opacity:.82}
#bar .meta{color:var(--gray);font-size:.85rem}
#bar .btn{margin-left:auto;color:var(--silver);text-decoration:none;border:1px solid var(--border);border-radius:.4rem;padding:.3rem .75rem;font-size:.85rem}
#bar .btn:hover{border-color:var(--amber);color:var(--amber)}
#cy{position:fixed;top:52px;left:0;right:0;bottom:0}
.panel{position:fixed;background:rgba(20,28,36,.92);border:1px solid var(--border);border-radius:.5rem;z-index:5}
#legend{left:18px;bottom:18px;padding:.6rem .85rem;font-size:.8rem}
#legend .row{display:flex;align-items:center;gap:.5rem;margin:.28rem 0}
#legend .dot{width:11px;height:11px;border-radius:50%;flex:0 0 auto}
#hint{right:18px;bottom:18px;padding:.45rem .7rem;color:var(--gray);font-size:.78rem}
</style>
</head>
<body>
<div id="bar">
<a href="/" class="brand" title="ontoref home">ontoref</a>
<span class="meta">ontology graph · __NCOUNT__ nodes · __ECOUNT__ edges</span>
<a href="/about" class="btn">← About</a>
</div>
<div id="cy"></div>
<div id="legend" class="panel">
<div class="row"><span class="dot" style="background:#E8A838"></span>Axioms · __NAX__</div>
<div class="row"><span class="dot" style="background:#C0CCD8"></span>Tensions · __NTN__</div>
<div class="row"><span class="dot" style="background:#7fc8e0"></span>Practices · __NPR__</div>
</div>
<div id="hint" class="panel">drag to pan · scroll to zoom · click a node to focus · click empty space to reset</div>
<script type="application/json" id="graph-data">/*__ELEMENTS__*/</script>
<script src="/js/cytoscape.min.js"></script>
<script src="/js/layout-base.js"></script>
<script src="/js/cose-base.js"></script>
<script src="/js/cytoscape-fcose.js"></script>
<script>
(function(){
var COL = { Axiom:"#E8A838", Tension:"#C0CCD8", Practice:"#7fc8e0" };
var data = JSON.parse(document.getElementById("graph-data").textContent);
var cy = cytoscape({
container: document.getElementById("cy"),
elements: data,
wheelSensitivity: 0.2,
style: [
{ selector:"node", style:{
"background-color": function(ele){ return COL[ele.data("level")] || "#7fc8e0"; },
"width": function(ele){ return ele.data("level")==="Axiom" ? 30 : (ele.data("level")==="Tension" ? 26 : 20); },
"height": function(ele){ return ele.data("level")==="Axiom" ? 30 : (ele.data("level")==="Tension" ? 26 : 20); },
"label":"data(title)", "color":"#C0CCD8", "font-size":"9px",
"text-valign":"bottom", "text-halign":"center", "text-margin-y":3,
"min-zoomed-font-size":7, "border-width":1, "border-color":"#0F1319"
} },
{ selector:"edge", style:{ "width":1, "line-color":"#46607c", "opacity":0.34, "curve-style":"haystack" } },
{ selector:".faded", style:{ "opacity":0.07, "text-opacity":0.07 } },
{ selector:"node.hi", style:{ "border-width":2, "border-color":"#E8A838", "opacity":1, "text-opacity":1 } },
{ selector:"edge.hi", style:{ "line-color":"#E8A838", "opacity":0.85, "width":1.6 } }
]
});
cy.minZoom(0.15); cy.maxZoom(3);
if (window.cytoscapeFcose) { try { cytoscape.use(window.cytoscapeFcose); } catch(e){} }
// fcose with packComponents:true PACKS the 5 edgeless orphan nodes right next to
// the main component (no scattered corners, no two groups); nodeDimensionsInclude-
// Labels spaces by label box (kills overlap); fit + post-fit fills the viewport.
var COMMON = { animate:false, fit:true, padding:50, nodeDimensionsIncludeLabels:true };
var layoutOpts = Object.assign({
name:"fcose", quality:"proof", randomize:true, packComponents:true,
idealEdgeLength:80, nodeSeparation:95, gravity:0.25, numIter:2500
}, COMMON);
var l;
try { l = cy.layout(layoutOpts); }
catch(e){ l = cy.layout(Object.assign({ name:"cose", componentSpacing:90, nodeRepulsion:500000 }, COMMON)); }
l.one("layoutstop", function(){ cy.fit(undefined, 50); });
l.run();
cy.on("tap","node",function(ev){
var n = ev.target;
var nb = n.closedNeighborhood();
cy.elements().addClass("faded");
nb.removeClass("faded").addClass("hi");
});
cy.on("tap", function(ev){ if (ev.target === cy){ cy.elements().removeClass("faded hi"); } });
})();
</script>
</body>
</html>
'##
def main [
--root: string = "../.."
--out: string = "site/public/images/ontoref-graph.html"
] {
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 [])
let ids = ($nodes | each {|n| $n.id? | default "" } | where {|x| $x | is-not-empty })
let node_els = ($nodes | each {|n| { data: {
id: ($n.id? | default ""),
title: ($n.name? | default ($n.id? | default "")),
level: ($n.level? | default "Practice"),
} } })
let edge_els = ($edges | where {|e| (($e.from? | default "") in $ids) and (($e.to? | default "") in $ids) } | each {|e| { data: {
id: $"($e.from)__($e.to)__($e.kind? | default 'rel')",
source: ($e.from? | default ""),
target: ($e.to? | default ""),
kind: ($e.kind? | default ""),
} } })
let elements = ($node_els | append $edge_els | to json --raw)
let nax = ($nodes | where {|n| ($n.level? | default "") == "Axiom" } | length)
let ntn = ($nodes | where {|n| ($n.level? | default "") == "Tension" } | length)
let npr = ($nodes | where {|n| ($n.level? | default "") == "Practice" } | length)
let html = ($TEMPLATE
| str replace --all '__NCOUNT__' ($nodes | length | into string)
| str replace --all '__ECOUNT__' ($edge_els | length | into string)
| str replace --all '__NAX__' ($nax | into string)
| str replace --all '__NTN__' ($ntn | into string)
| str replace --all '__NPR__' ($npr | into string)
| str replace '/*__ELEMENTS__*/' $elements)
let parent = ($out | path dirname)
if ($parent | is-not-empty) { mkdir $parent }
$html | save -f $out
print $"gen-graph-page: ($nodes | length) nodes · ($edge_els | length) edges → ($out)"
}

View file

@ -0,0 +1,217 @@
#!/usr/bin/env nu
# Generate the four-door domain spine from the positioning graph (bl-035, ADR-064/065).
#
# Consumes the structured projection contract (.ontoref/positioning/spine.ncl)
# and each referenced value-prop, then emits the four doors as posts of the
# `dominios` content-kind — these are ontoref's DOMAINS, not projects (ADR-065).
# `dominios` content is regenerated here + reindexed by content_processor, BUT the
# `/dominios` + `/dominios/{slug}` ROUTES are baked: crates/server/build.rs codegen
# reads content-kinds.{toml,ncl} at BUILD time. Empirically verified — a fresh
# restart with `dominios` in content-kinds.toml still 404s. So activating the kind
# needs `dominios` in content.ncl + content-kinds.ncl AND a website-htmx-rustelo
# rebuild + reinstall (the catalog kind required the same; the route is not runtime).
#
# Layout (category-layout — matches the loader's body-path resolution
# `{lang}/{category}/{id}.md`; URL slug stays the frontmatter `slug`):
# content/domains/<lang>/use-cases/door-<slug>.md → /domains/<slug> (EN) · /dominios/<slug> (ES)
#
# The hook (door landings) is static NCL→build; reveal rungs 1 & 3 (live
# describe-impact, browsable graph) link to the running daemon — the site stays
# usable without it. Hero/sub-hero/close are emitted as _spine fragments for
# manual wiring into the Rust home page component (they need a handler, not a kind).
#
# Usage:
# nu scripts/build/gen-spine-pages.nu --root <constellation-root> --content <content dir> [--daemon <url>] [--date <iso>]
def main [
--root: string = "../.." # constellation root, relative to outreach/site
--content: string = "site/content" # site content dir, relative to outreach/site
--daemon: string = "https://ontoref.dev" # base URL the live-graph links resolve against
--date: string = "2026-06-10" # publication date stamped on generated posts
--langs: list<string> = ["es" "en"]
] {
let spine_path = $"($root)/.ontoref/positioning/spine.ncl"
let vp_dir = $"($root)/.ontoref/positioning/value-props"
if not ($spine_path | path exists) {
error make { msg: $"spine contract not found: ($spine_path)" }
}
let spine = (nickel export --format json $spine_path | from json)
for lang in $langs {
let is_es = ($lang == "es")
let kind_path = (if $is_es { "dominios" } else { "domains" })
# ── Doors as `domains` posts (routable: /domains/{slug} EN · /dominios/{slug} ES) ──
for item in ($spine.doors | enumerate) {
let d = $item.item
let vp = (nickel export --format json $"($vp_dir)/($d.value_prop_id).ncl" | from json)
let label = ($d.label | get $lang)
let line = ($d.line | get $lang)
let blog_url = $"/blog?tag=($d.tag)"
let recipes_url = (if $is_es { $"/recetas?tag=($d.tag)" } else { $"/recipes?tag=($d.tag)" })
let prize_line = (if $is_es { $spine.prize.line } else { $spine.prize.en })
# CTA routes to real content tagged for this door (blog + recipes), not the daemon
# graph — reach-vs-qualification: route to a live, populated surface.
let cta = (if $is_es {
$"[Posts]\(($blog_url)\) · [Recetas]\(($recipes_url)\)"
} else {
$"[Posts]\(($blog_url)\) · [Recipes]\(($recipes_url)\)"
})
let prize_label = (if $is_es { "El premio, al entrar" } else { "The prize, once inside" })
let excerpt = ($vp.problem_solved | lines | where ($it | str trim | is-not-empty) | first | str trim | str replace --all '"' "'")
let cat_desc = (if $is_es { "Casos de uso e implementaciones de ontoref" } else { "Use cases and implementations of ontoref" })
let proj_dir = $"($content)/domains/($lang)/use-cases"
mkdir $proj_dir
let front = $"---
id: \"door-($d.key)\"
title: \"($label)\"
slug: \"($d.key)\"
subtitle: \"($line)\"
excerpt: \"($excerpt)\"
author: \"ontoref\"
date: \"($date)\"
published: true
featured: true
category: \"use-cases\"
tags: [\"($d.key)\", \"ontoref\", \"use-case\"]
read_time: \"3 min read\"
sort_order: ($item.index + 1)
css_class: \"category-use-cases\"
category_description: \"($cat_desc)\"
category_published: true
---
"
# Cold-open evidence hook (spine.evidence_hooks) — the market's own number
# as the honest gancho above the claim, routing to the door's live graph
# (route-to-live-graph principle). Inert when no hook targets this door.
let hook = ($spine.evidence_hooks? | default [] | where {|h| $h.door == $d.key })
let hook_block = (if ($hook | is-empty) { "" } else {
let h = ($hook | first)
let hline = ($h.hook | get $lang)
let see = (if $is_es { "Comprueba qué se rompe" } else { "See what breaks" })
let see_url = $"/blog?tag=($d.tag)"
$"> **($hline)**
>
> — [($h.source.name)]\(($h.source.url)\) · [($see)]\(($see_url)\)
"
})
let body = $"($hook_block)**($vp.claim)**
($vp.problem_solved)
---
($cta)
### ($prize_label)
> ($prize_line)
"
$"($front)\n($body)" | save -f $"($proj_dir)/door-($d.key).md"
# Sidecar .ncl carrying the `graph` block read by gen-content-graph (the foot
# mini-graph). gen-content-graph globs **/*.ncl (never the .md), so without this
# the door has no non-tag edges and its sidebar renders empty. Emitted once on
# the en pass — the mini graph is keyed by content id, language-agnostic.
if not $is_es {
let adr_short = ($vp.evidence_adrs | each {|a| ($a | split row "-" | take 2 | str join "-") })
let implements = ($adr_short | append ["positioning-layer", "reach-vs-qualification"])
let related = ($spine.doors | each {|x| $x.key } | where {|k| $k != $d.key } | each {|k| $"door-($k)" })
let impl_arr = ($implements | each {|x| $"\"($x)\"" } | str join ", ")
let rel_arr = ($related | each {|x| $"\"($x)\"" } | str join ", ")
$"{
id = \"door-($d.key)\",
title = \"($label)\",
slug = \"($d.key)\",
excerpt = \"($excerpt)\",
tags = [\"($d.key)\", \"ontoref\", \"use-case\"],
page_route = \"/domains/($d.key)\",
graph = {
implements = [($impl_arr)],
related_to = [($rel_arr)],
},
}
" | save -f $"($proj_dir)/door-($d.key).ncl"
}
}
# ── Home hero + close as _spine fragments (manual wiring into Rust page) ───
let frag_dir = $"($content)/domains/_spine"
mkdir $frag_dir
let hero_line = (if $is_es { $spine.hero.line } else { $spine.hero.en })
let sub_line = (if $is_es { $spine.sub_hero.line } else { $spine.sub_hero.en })
let close_line = (if $is_es { $spine.close.line } else { $spine.close.en })
let door_cards = (
$spine.doors
| each {|d|
let label = ($d.label | get $lang)
let dline = ($d.line | get $lang)
let slug_path = $"/($kind_path)/($d.key)"
$"- **[($label)]\(($slug_path)\)** — ($dline)"
}
| str join "\n"
)
let heading = (if $is_es { "Cuatro puertas, el mismo edificio" } else { "Four doors, one building" })
$"<!-- generated from .ontoref/positioning/spine.ncl — wire into the home/Ontoref page component -->
# ($hero_line)
> ($sub_line)
## ($heading)
($door_cards)
---
_($close_line)_
" | save -f $"($frag_dir)/home-($lang).md"
# ── Spine lines projected into i18n (`spine-*` keys, owned by this generator;
# hand-maintained protocol-mechanics keys stay in pages/home.ftl) ──
let prize_line = (if $is_es { $spine.prize.line } else { $spine.prize.en })
let i18n_pages = ($content | path dirname | path join "i18n" "locales" $lang "pages")
mkdir $i18n_pages
# Evidence-hook banner lines, one fluent key per hooked door (spine-hook-<door>).
let hook_keys = (
$spine.evidence_hooks? | default []
| each {|h| let hl = (if $is_es { $h.hook.es } else { $h.hook.en }); $"spine-hook-($h.door) = ($hl)" }
| str join "\n"
)
# Per-door panel keys for the home vestibule (ADR-057): label + imperative line
# + the door's own landing href (lang-aware) + live-graph url. The panel is a
# teaser that ROUTES (reach-vs-qualification); the full value-prop lives on the
# door landing the href points to.
let door_keys = (
$spine.doors
| each {|d|
let dlabel = ($d.label | get $lang)
let dline = ($d.line | get $lang)
let dhref = (if $is_es { $"/dominios/($d.key)" } else { $"/domains/($d.key)" })
# door CTA routes to real content tagged for this door (blog + recipes),
# not to the daemon graph — reach-vs-qualification: route to a live surface.
let dblog = $"/blog?tag=($d.tag)"
let drecipes = (if $is_es { $"/recetas?tag=($d.tag)" } else { $"/recipes?tag=($d.tag)" })
$"spine-door-($d.key)-label = ($dlabel)\nspine-door-($d.key)-line = ($dline)\nspine-door-($d.key)-href = ($dhref)\nspine-door-($d.key)-blog = ($dblog)\nspine-door-($d.key)-recipes = ($drecipes)"
}
| str join "\n"
)
let door_cta = (if $is_es { "spine-door-cta = Ver más\nspine-door-blog-cta = Posts\nspine-door-recipes-cta = Recetas" } else { "spine-door-cta = See more\nspine-door-blog-cta = Posts\nspine-door-recipes-cta = Recipes" })
$"# GENERATED from .ontoref/positioning/spine.ncl by gen-spine-pages.nu — do not edit by hand.
spine-hero = ($hero_line)
spine-sub = ($sub_line)
spine-close = ($close_line)
spine-prize = ($prize_line)
spine-doors-heading = ($heading)
($door_cta)
($door_keys)
($hook_keys)
" | save -f ($i18n_pages | path join "home_spine.ftl")
}
print $"spine generated: doors as `dominios` posts + _spine fragments for langs ($langs | str join ', ')"
}

View file

@ -0,0 +1,101 @@
#!/usr/bin/env nu
# Project the curated definition-phrases (.ontoref/positioning/taglines.ncl) into a
# grouped JSON the hero rotator consumes (typed.js / CSS-fade). The phrases are typed
# and drift-checked by taglines-schema.ncl at export — an unknown audience, or a focus
# that is not a real differentiator id, fails here and never reaches the page
# (diff-reflexive-no-drift).
#
# Output shape (by_audience is the contract the rotator binds to):
# { generated_from, audiences: [<present keys>],
# by_audience: { "all": [{id,es,en,focus,register}], "arriving-developer": [...], ... } }
#
# Driver semantics the JSON supports: home starts on by_audience.all (neutral
# auto-cycle); a door click fixes the rotator to that audience's queue; door/audience
# pages mount with a fixed key. "all" is the curated neutral queue, NOT the union —
# reach-vs-qualification: a routing gancho, never a wall of every phrase.
#
# Usage:
# nu gen-taglines.nu --root <constellation-root> --out <taglines.json path>
def main [
--root: string = "../.." # constellation root, relative to caller PWD
--out: string = "site/public/r/taglines.json" # output JSON, relative to caller PWD
] {
let src = $"($root)/.ontoref/positioning/taglines.ncl"
if not ($src | path exists) {
error make { msg: $"taglines source not found: ($src)" }
}
# `nickel export` applies TaglinesShape|TaglinesCap|TaglinesUniqueIds — drift fails here.
let data = (nickel export --format json $src | from json)
# Canonical audience order (mirror taglines-schema.ncl AudienceIds). "all" leads so
# the home neutral queue comes first; the rest serve door/audience pages.
let order = [
"all"
"arriving-developer"
"infrastructure-inheritor"
"personal-ontology"
"compliance-officer"
"engineering-leader"
"platform-engineer"
]
# Consumer-facing shape — drop provenance (source); keep what the rotator renders.
let project = {|t|
{
id: $t.id,
es: $t.es,
en: $t.en,
focus: ($t.focus? | default null),
register: $t.register,
}
}
let by_audience = (
$order
| reduce --fold {} {|aid, acc|
let items = ($data.taglines | where audience_id == $aid | each $project)
$acc | insert $aid $items
}
)
let present = ($order | where {|aid| ($by_audience | get $aid | length) > 0 })
# Localized focus labels (es/en) for the 6 differentiator ids — the rotator renders
# these next to each phrase and re-renders on language change. Slug fallback in the JS.
let focus_labels = {
"diff-accredited-knowledge": { es: "conocimiento acreditado", en: "accredited knowledge" },
"diff-agent-native": { es: "nativo para agentes", en: "agent-native" },
"diff-domain-transversality": { es: "transversalidad de dominio", en: "domain transversality" },
"diff-graduated-adoption": { es: "adopción graduada", en: "graduated adoption" },
"diff-reflexive-no-drift": { es: "coherencia reflexiva", en: "reflexive coherence" },
"diff-witness-seam": { es: "costura testigo", en: "witness seam" },
}
# Tier taglines — a PARALLEL axis (adoption tier, not audience): projected from
# tier-taglines.ncl into `by_tier` so the hero's tier rotator (data-tier-rotator)
# cycles the per-tier lines. Empty if the file is absent (opt-in, ADR-029).
let tier_src = $"($root)/.ontoref/positioning/tier-taglines.ncl"
let by_tier = (
if ($tier_src | path exists) {
nickel export --format json $tier_src | from json | get tier_taglines
| reduce --fold {} {|t, acc|
$acc | insert ($t.tier | into string) [{ id: $"tier-($t.tier)", es: $t.es, en: $t.en, focus: null, register: "Tier" }]
}
} else { {} }
)
let payload = {
generated_from: ".ontoref/positioning/taglines.ncl",
audiences: $present,
by_audience: $by_audience,
by_tier: $by_tier,
focus_labels: $focus_labels,
}
let out_dir = ($out | path dirname)
if ($out_dir | str length) > 0 { mkdir $out_dir }
$payload | to json --indent 2 | save -f $out
print $"taglines projected → ($out) [($data.taglines | length) phrases · audiences: ($present | str join ', ')]"
}

View file

@ -0,0 +1,139 @@
#!/usr/bin/env nu
# Shared page chrome for projected static doc surfaces (ADRs, catalog, …).
# One source of truth for the brand palette, dark/light theme, nav, and the
# common structural CSS (.doc-* classes). Consumed by gen-adr-pages.nu and
# gen-catalog-pages.nu so both outreach/web (static) and the SSR site share an
# identical look and feel.
export def esc [s: string] {
$s | str replace --all '&' '&amp;' | str replace --all '<' '&lt;' | str replace --all '>' '&gt;' | str replace --all '"' '&quot;'
}
export def paras [text: string] {
$text | split row "\n\n" | where {|b| ($b | str trim | is-not-empty) }
| each {|b| $"<p>(esc ($b | str trim))</p>" } | str join "\n"
}
# A single <section> with an anchor + h2; empty body collapses to nothing.
export def sec [anchor: string, title: string, body: string] {
if ($body | is-empty) { "" } else { $"<section id=\"($anchor)\" class=\"doc-section\"><h2>(esc $title)</h2>($body)</section>" }
}
# Brand-aligned CSS: ontoref palette tokens, dark default + html.light overrides,
# content-graph SVG var bindings, and the .doc-* structural classes.
export def page-css [] { r#'<style>
:root{
--onto-amber:#E8A838;--onto-amber-dark:#C88A20;--onto-amber-light:#F0C265;
--onto-silver:#C0CCD8;--onto-dark:#0F1319;--onto-gray:#8090A4;--onto-gray2:#a8b4c8;
--onto-surface:#141c24;--onto-border:#1e2a38;
--onto-amber-rgb:232,168,56;--onto-silver-rgb:192,204,216;--onto-dark-rgb:15,19,25;--onto-amber-dark-rgb:200,138,32;
--cg-node:#4a5568;--cg-ego:#E8A838;--cg-onto:#9b59b6;
--cg-adr:#E8A838;--cg-edge:#1e2a38;--cg-label:#C0CCD8;
}
html.light{
--onto-dark:#F0F2F5;--onto-surface:#FFFFFF;--onto-border:#d1d9e0;
--onto-silver:#1a2030;--onto-gray:#5a6a7c;--onto-gray2:#374151;
--cg-node:#6b7280;--cg-ego:#C88A20;--cg-edge:#d1d5db;--cg-label:#374151;
}
*{box-sizing:border-box}
body{font-family:Inter,-apple-system,BlinkMacSystemFont,sans-serif;background:var(--onto-dark);color:var(--onto-silver);margin:0;transition:background .2s,color .2s}
a{color:var(--onto-amber);text-decoration:none}a:hover{text-decoration:underline}
h1{color:var(--onto-silver)}
.doc-nav{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1.5rem;border-bottom:1px solid var(--onto-border);background:var(--onto-dark);position:sticky;top:0;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:10}
.doc-nav-logo{font-weight:700;color:var(--onto-amber);text-decoration:none;font-family:monospace;font-size:1.1rem;letter-spacing:-.02em}
.doc-nav-ctl{display:flex;gap:.5rem;align-items:center}
.doc-nav-btn{padding:.25rem .75rem;border-radius:.4rem;text-decoration:none;border:1px solid var(--onto-border);color:var(--onto-silver);font-size:.85rem;background:none}
.doc-nav-btn:hover{border-color:var(--onto-amber);color:var(--onto-amber);text-decoration:none}
.theme-btn{background:none;border:1px solid var(--onto-border);border-radius:.4rem;padding:.25rem .5rem;cursor:pointer;color:var(--onto-silver);font-size:.9rem;line-height:1}
.theme-btn:hover{border-color:var(--onto-amber)}
.doc-wrap{max-width:52rem;margin:0 auto;padding:2rem 1rem}
.doc-head{border-bottom:1px solid var(--onto-border);padding-bottom:1rem;margin-bottom:1.5rem}
.doc-badge{display:inline-block;padding:.15rem .6rem;border-radius:.4rem;font:600 .75rem/1.4 monospace;margin-right:.35rem}
.doc-st-accepted{background:rgba(31,120,74,.2);color:#7fe0a8}
.doc-st-proposed{background:rgba(232,168,56,.15);color:#E8A838}
.doc-st-superseded,.doc-st-deprecated{background:rgba(224,128,128,.15);color:#e08080}
.doc-sev-hard{background:rgba(224,128,128,.15);color:#e08080}
.doc-sev-soft{background:var(--onto-surface);color:var(--onto-gray2)}
.doc-tag-structural{background:rgba(155,89,182,.15);color:#c39bd3}
.doc-tag-contextual{background:rgba(232,168,56,.15);color:#E8A838}
.doc-tag-operation{background:rgba(64,160,200,.15);color:#7fc8e0}
.doc-tag-validator{background:rgba(127,224,168,.12);color:#7fe0a8}
.doc-meta{font-family:monospace;opacity:.6;margin-left:.5rem;font-size:.9rem}
.doc-section{margin:1.5rem 0}
.doc-section h2{color:var(--onto-amber);font-size:1.1rem;margin-bottom:.5rem;font-weight:600}
.doc-section p{line-height:1.7;margin-bottom:.75rem}
.doc-list{list-style:none;padding:0}
.doc-list>li{display:flex;gap:.6rem;padding:.5rem 0;border-bottom:1px solid var(--onto-border);line-height:1.6}
.doc-alt-opt{font-weight:600;margin-bottom:.2rem}
.doc-alt-why{color:var(--onto-gray2);margin:0}
.doc-kv{width:100%;border-collapse:collapse;font-size:.9rem}
.doc-kv td{padding:.4rem .6rem;border-bottom:1px solid var(--onto-border);vertical-align:top}
.doc-kv td:first-child{font-family:monospace;color:var(--onto-gray2);white-space:nowrap;width:1%}
.doc-code{font-family:'JetBrains Mono',Consolas,monospace;font-size:.85em;color:var(--onto-amber-light)}
.doc-related a{font-family:monospace;color:var(--onto-amber)}
.doc-toc{border:1px solid var(--onto-border);border-radius:.5rem;padding:.75rem 1rem;margin:1rem 0;background:var(--onto-surface)}
.doc-toc p{font:600 .7rem/1 monospace;text-transform:uppercase;opacity:.5;margin:0 0 .5rem}
.doc-toc ul{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:.5rem}
.doc-toc a{font-size:.8rem;color:var(--onto-silver);text-decoration:none}
.doc-toc a:hover{color:var(--onto-amber)}
.doc-search{width:100%;padding:.6rem .9rem;margin:0 0 1rem;border-radius:.5rem;border:1px solid var(--onto-border);background:var(--onto-surface);color:var(--onto-silver);font-size:.95rem}
.doc-search::placeholder{color:var(--onto-gray)}
.doc-search:focus{outline:none;border-color:var(--onto-amber)}
.doc-mini-graph{margin:1.5rem 0;border:1px solid var(--onto-border);border-radius:.5rem;overflow:hidden;background:var(--onto-surface);padding:.5rem}
.doc-mini-graph .content-graph-mini{width:100%;max-width:360px;display:block;margin:0 auto;height:auto}
.doc-idx-group{margin:1.5rem 0}
.doc-idx-group h2{color:var(--onto-amber);font-size:1rem;font-weight:600;margin-bottom:.5rem;font-family:monospace}
.doc-idx-list{list-style:none;padding:0}
.doc-idx-list>li{padding:.4rem 0;border-bottom:1px solid var(--onto-border)}
.doc-idx-list>li.hidden{display:none}
.doc-idx-list a{color:var(--onto-silver);display:block}
.doc-idx-list a:hover{color:var(--onto-amber)}
.doc-idx-list a code{font-size:.8rem;color:var(--onto-gray2);font-family:monospace;margin-right:.4rem}
.no-results{display:none;color:var(--onto-gray);font-style:italic;padding:1rem 0}
</style>'# }
# Persisted theme toggle, shared across all doc pages.
export def theme-js [] { r#'<script>
(function(){
var K="ontoref-theme";
function getT(){return localStorage.getItem(K)||"dark"}
function setT(t){
localStorage.setItem(K,t);
document.documentElement.classList.toggle("light",t==="light");
var b=document.getElementById("theme-btn");
if(b)b.textContent=t==="light"?"🌙":"☀️";
}
window.toggleTheme=function(){setT(getT()==="dark"?"light":"dark")};
setT(getT());
})();
</script>'# }
# Full HTML document: brand chrome + sibling-surface nav (ADRs · Catalog) + body.
export def page-shell [title: string, content: string] {
$"<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<title>(esc $title) · Ontoref</title>
<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap\" rel=\"stylesheet\">
(page-css)
</head>
<body>
<nav class=\"doc-nav\">
<a class=\"doc-nav-logo\" href=\"/\">ontoref</a>
<div class=\"doc-nav-ctl\">
<a class=\"doc-nav-btn\" href=\"/adr/\">ADRs</a>
<a class=\"doc-nav-btn\" href=\"/catalog/\">Catalog</a>
<button id=\"theme-btn\" class=\"theme-btn\" onclick=\"toggleTheme\(\)\" title=\"Toggle theme\">☀️</button>
</div>
</nav>
<div class=\"doc-wrap\">
($content)
</div>
(theme-js)
</body>
</html>"
}

View file

@ -0,0 +1,84 @@
#!/usr/bin/env nu
# Linkify ADR mentions in outreach/web/src/*.html.
#
# Two passes per file:
# 1. Fix href format: absolute https://ontoref.dev/adr/adr-NNN → site-relative /adr/NNN
# 2. Wrap plain ADR-NNN text (not already inside <a ...>...</a>) with a link
#
# All hrefs use site-relative paths (/adr/NNN) so links work in both localhost
# and production without domain changes.
# Single-quotes are used for injected href/title/class attributes so the link
# is safe inside both text nodes and double-quoted data-en/data-es attributes.
#
# Usage (from outreach/site):
# nu scripts/build/linkify-adrs.nu
# nu scripts/build/linkify-adrs.nu --map site/public/r/adr-map.json --web ../../outreach/web --daemon https://ontoref.dev
def escape-attr [s: string] {
$s
| str replace --all '&' '&amp;'
| str replace --all '<' '&lt;'
| str replace --all '>' '&gt;'
| str replace --all "'" '&apos;'
| str replace --all '"' '&quot;'
}
def main [
--map: string = "site/public/r/adr-map.json"
--web: string = "../../outreach/web"
--daemon: string = "https://ontoref.dev" # used only to recognise existing absolute hrefs to fix
] {
let adr_map = (open $map)
let web_dir = ($web | path expand)
let src_dir = ($web_dir | path join "src")
if not ($src_dir | path exists) {
error make { msg: $"src dir not found: ($src_dir)" }
}
let files = (glob $"($src_dir)/*.html" | sort)
mut total_files = 0
mut total_changes = 0
for f in $files {
mut content = (open --raw $f)
let original = $content
# Sort ADRs longest-id-first so adr-010 doesn't shadow adr-0100 (hypothetical),
# and to process higher numbers first (safer for text replacement ordering).
let adrs = ($adr_map | transpose key val | sort-by key --reverse)
for entry in $adrs {
let id = $entry.key # "adr-009"
let val = $entry.val
let num = ($id | str replace 'adr-' '') # "009"
let adr_upper = $"ADR-($num)" # "ADR-009"
let path = $"/adr/($num)" # "/adr/009" — site-relative, works everywhere
let safe_name = (escape-attr ($val.name? | default ""))
# Pass 1: fix existing absolute hrefs to the canonical site-relative path.
# Handles double-quoted (original manual links) and single-quoted (injected links).
$content = ($content | str replace --all $"href=\"($daemon)/adr/($id)\"" $"href=\"($path)\"")
$content = ($content | str replace --all $"href=\"($daemon)/adr/($num)\"" $"href=\"($path)\"")
$content = ($content | str replace --all $"href='($daemon)/adr/($id)'" $"href='($path)'")
$content = ($content | str replace --all $"href='($daemon)/adr/($num)'" $"href='($path)'")
# Pass 2: linkify plain ADR-NNN not already followed by </a>
# Uses single-quote attrs: safe inside double-quoted HTML attribute values.
let link = $"<a href='($path)' title='($safe_name)' class='adr-ref' target='_blank' rel='noopener'>($adr_upper)</a>"
let pattern = $"ADR-($num)\(?!</a>\)"
$content = ($content | str replace --all --regex $pattern $link)
}
if $content != $original {
$content | save -f $f
let linked_after = ($content | find --regex "ADR-[0-9]{3}</a>" | length)
print $" ($f | path basename): updated"
$total_files = ($total_files + 1)
$total_changes = ($total_changes + 1)
}
}
print $"linkify-adrs: ($total_files) files updated"
}

View file

@ -0,0 +1,116 @@
#!/usr/bin/env nu
# Linkify site conventions in blog markdown source:
#
# ontoref / Ontoref → <a href="/about" class="brand-site">…</a> (brand mention, case-preserving)
# ADR-NNN → <a href="/adr/NNN" class="adr-ref">ADR-NNN</a> (decision deep-link)
#
# Operates on the MARKDOWN SOURCE (not the generated HTML) so the change survives
# every `just content` regeneration. Both rewrites are idempotent via lookarounds:
# an already-wrapped mention (preceded by `>` / followed by `</a>`) is left as-is.
#
# Skipped, so code and metadata never get a stray link:
# - YAML frontmatter (between the leading --- delimiters)
# - fenced code blocks (``` … ```)
# - inline code spans (`…`)
# - headings (lines beginning with #)
# - compound identifiers: ontoref-daemon, ontoref/dist, .ontoref, /adr/038 (lookarounds)
#
# Usage (from outreach/site):
# nu scripts/build/linkify-site.nu # default glob
# nu scripts/build/linkify-site.nu site/content/blog/**/*.md
# nu scripts/build/linkify-site.nu --check # CI gate, no writes
const LINK = '<a href="/about" class="brand-site">${n}</a>'
const WORD = '(?<![>\w./-])(?<n>[Oo]ntoref)(?![<\w./-])'
const ADR_LINK = '<a href="/adr/${num}" class="adr-ref">ADR-${num}</a>'
const ADR_WORD = '(?<![>/\w-])ADR-(?<num>\d+)(?!</a>)'
# Apply both site rewrites outside inline-code spans on a single line.
def linkify-line [line: string]: nothing -> string {
$line
| split row '`'
| enumerate
| each {|it|
if ($it.index mod 2) == 0 {
$it.item
| str replace --all --regex $WORD $LINK
| str replace --all --regex $ADR_WORD $ADR_LINK
} else {
$it.item
}
}
| str join '`'
}
def main [
pattern: string = "site/content/blog/**/*.md"
--check # report posts that WOULD change and exit non-zero; never writes
] {
let files = (glob $pattern | sort)
mut changed = 0
for f in $files {
let lines = (open --raw $f | lines)
mut out = []
mut in_front = false
mut in_fence = false
mut idx = 0
for line in $lines {
let trimmed = ($line | str trim)
# Frontmatter: a leading --- opens it, the next --- closes it.
if $trimmed == "---" and not $in_fence {
if $idx == 0 {
$in_front = true
} else if $in_front {
$in_front = false
}
$out = ($out | append $line)
$idx = ($idx + 1)
continue
}
if ($trimmed | str starts-with '```') {
$in_fence = (not $in_fence)
$out = ($out | append $line)
$idx = ($idx + 1)
continue
}
if $in_front or $in_fence or ($trimmed | str starts-with '#') {
$out = ($out | append $line)
$idx = ($idx + 1)
continue
}
$out = ($out | append (linkify-line $line))
$idx = ($idx + 1)
}
let result = ($out | str join "\n")
let original = (open --raw $f)
# Preserve a trailing newline if the source had one.
let final = if ($original | str ends-with "\n") { $result + "\n" } else { $result }
if $final != $original {
if $check {
print $" ($f | path basename): unlinked site mentions pending"
} else {
$final | save -f $f
print $" ($f | path basename): linked"
}
$changed = ($changed + 1)
}
}
if $check {
if $changed > 0 {
error make { msg: $"linkify-site --check: ($changed) post\(s\) have unlinked site mentions — run `just content`" }
}
print "linkify-site --check: all posts clean"
} else {
print $"linkify-site: ($changed) file\(s\) updated"
}
}

View file

@ -0,0 +1,53 @@
#!/usr/bin/env nu
# Typecheck gate for the About projection contract (about-schema.ncl).
# This is the ⛓ design gate: it must pass before any gen-about-pages projector
# or /about handler work begins. Verifies:
# 1. the real ontoref about.ncl exports (positive)
# 2. a valid 'Personal instance exports (both kinds expressible)
# 3. 'Project with a stray `personal` block is rejected (coherence)
# 4. 'Personal missing its `personal` block is rejected (coherence)
#
# Usage (from outreach/site):
# nu scripts/build/test-about-contract.nu
# nu scripts/build/test-about-contract.nu --root ../..
def exports [dir: string, body: string] {
let tmp = $"($dir)/.about-contract-test.ncl"
$body | save -f $tmp
let ok = (do { nickel export --format json $tmp } | complete | get exit_code) == 0
rm -f $tmp
$ok
}
def main [--root: string = "../.."] {
let pos_dir = $"($root)/.ontoref/positioning"
if not ($"($pos_dir)/about-schema.ncl" | path exists) {
error make { msg: $"about-schema.ncl not found under ($pos_dir)" }
}
mut failures = []
# 1 — real instance exports
let real_ok = (do { nickel export --format json $"($pos_dir)/about.ncl" } | complete | get exit_code) == 0
if not $real_ok { $failures = ($failures | append "about.ncl (ontoref, 'Project) failed to export") }
# 2 — valid personal instance
let personal_ok = (exports $pos_dir "let s = import \"about-schema.ncl\" in { kind = 'Personal, level_id = \"t\", personal = { template = \"about.j2\" } } | s.AboutShape | s.AboutCoherence")
if not $personal_ok { $failures = ($failures | append "valid 'Personal instance was rejected") }
# 3 — 'Project with stray personal block must be rejected
let proj_stray_rejected = not (exports $pos_dir "let s = import \"about-schema.ncl\" in { kind = 'Project, level_id = \"t\", personal = { template = \"about.j2\" } } | s.AboutShape | s.AboutCoherence")
if not $proj_stray_rejected { $failures = ($failures | append "'Project with stray personal block was accepted (coherence breach)") }
# 4 — 'Personal missing its block must be rejected
let personal_missing_rejected = not (exports $pos_dir "let s = import \"about-schema.ncl\" in { kind = 'Personal, level_id = \"t\" } | s.AboutShape | s.AboutCoherence")
if not $personal_missing_rejected { $failures = ($failures | append "'Personal missing its block was accepted (coherence breach)") }
if ($failures | is-empty) {
print "about-contract: 4/4 checks pass — ⛓ gate OPEN"
} else {
for f in $failures { print $" ✗ ($f)" }
error make { msg: $"about-contract: ($failures | length) check\(s\) failed — ⛓ gate CLOSED" }
}
}

View file

@ -0,0 +1,27 @@
# website-impl Public Assets
This directory contains static assets served directly by the web server.
## Directory Structure
```
public/
├── images/ # Static images (logos, icons, etc.)
├── scripts/ # Client-side JavaScript
├── styles/ # Public CSS files
├── documents/ # Downloadable documents
└── README.md # This file
```
## Usage
Files in this directory are served at the root URL:
- `public/logo.png``https://yoursite.com/logo.png`
- `public/styles/custom.css``https://yoursite.com/styles/custom.css`
## Guidelines
- Keep file sizes optimized for web delivery
- Use descriptive filenames
- Organize by type (images, scripts, styles, documents)
- Consider CDN deployment for production

View file

@ -0,0 +1,95 @@
/**
* Console Logger - Captures all browser console output to localStorage
* This allows viewing logs from /public/logs.html across all pages
*/
(function() {
// Capture all console methods
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
const originalInfo = console.info;
const originalDebug = console.debug;
function formatTimestamp() {
return new Date().toISOString();
}
function captureLog(level, args) {
const message = args.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2);
} catch (e) {
return String(arg);
}
}
return String(arg);
}).join(' ');
const logEntry = {
timestamp: formatTimestamp(),
level,
message
};
// Save to localStorage for logs viewer
try {
const stored = localStorage.getItem('console_logs');
let logs = stored ? JSON.parse(stored) : [];
logs.push(logEntry);
// Keep last 100 logs in localStorage
if (logs.length > 100) {
logs = logs.slice(-100);
}
localStorage.setItem('console_logs', JSON.stringify(logs));
} catch (e) {
// localStorage might be unavailable or full - silently fail
}
}
// Override console methods
console.log = function(...args) {
originalLog(...args);
captureLog('LOG', args);
};
console.error = function(...args) {
originalError(...args);
captureLog('ERROR', args);
};
console.warn = function(...args) {
originalWarn(...args);
captureLog('WARN', args);
};
console.info = function(...args) {
originalInfo(...args);
captureLog('INFO', args);
};
console.debug = function(...args) {
originalDebug(...args);
captureLog('DEBUG', args);
};
// Capture uncaught errors and unhandled promise rejections (module load failures go here)
window.addEventListener('error', function(e) {
const msg = e.message || 'Unknown error';
const src = e.filename ? ` @ ${e.filename}:${e.lineno}` : '';
captureLog('ERROR', [`[WINDOW-ERROR] ${msg}${src}`]);
originalError('[WINDOW-ERROR]', msg, src, e.error);
});
window.addEventListener('unhandledrejection', function(e) {
const reason = e.reason instanceof Error
? `${e.reason.message}\n${e.reason.stack}`
: String(e.reason);
captureLog('ERROR', [`[UNHANDLED-REJECTION] ${reason}`]);
originalError('[UNHANDLED-REJECTION]', e.reason);
});
// Initial message
originalLog('🔍 Console Logger initialized - capturing to localStorage');
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View file

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 387.4 354.41">
<defs>
<style>
.cls-1 {
stroke-width: 2px;
}
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9, .cls-10, .cls-11 {
stroke: #231f20;
}
.cls-1, .cls-2, .cls-10 {
fill: #e6e7e8;
}
.cls-1, .cls-4, .cls-5, .cls-6, .cls-7, .cls-9, .cls-10, .cls-11 {
stroke-miterlimit: 10;
}
.cls-2 {
stroke-width: 1.84px;
}
.cls-2, .cls-5, .cls-6, .cls-12, .cls-9, .cls-10 {
opacity: .38;
}
.cls-2, .cls-5, .cls-6, .cls-9, .cls-10 {
isolation: isolate;
}
.cls-3, .cls-13, .cls-11 {
fill: #fff;
}
.cls-4, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9 {
fill: none;
}
.cls-5 {
stroke-dasharray: 5.24 4.85;
}
.cls-14 {
letter-spacing: 0em;
}
.cls-6 {
stroke-dasharray: .84 1.39;
stroke-width: 1.11px;
}
.cls-15 {
opacity: .44;
}
.cls-7 {
stroke-dasharray: .67 1.11;
}
.cls-9, .cls-10 {
stroke-dasharray: 1.89 3.79;
stroke-width: .99px;
}
.cls-16 {
letter-spacing: 0em;
}
.cls-17 {
letter-spacing: 0em;
}
.cls-18 {
letter-spacing: -.01em;
}
.cls-19 {
font-family: SignPainter-HouseScript, SignPainter;
font-size: 18px;
}
.cls-11 {
stroke-width: .25px;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1-2">
<g>
<path class="cls-2" d="M275.05,224.67c-26.21,45.05-83.99,60.32-129.05,34.11-45.07-26.22-60.34-83.99-34.13-129.05,26.22-45.06,84-60.34,129.06-34.12,45.06,26.21,60.34,83.99,34.12,129.06Z"/>
<path class="cls-2" d="M227.58,306.26c-26.21,45.06-83.99,60.33-129.05,34.12-45.07-26.21-60.34-84-34.13-129.05,26.21-45.06,84-60.34,129.06-34.12,45.06,26.21,60.33,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M322.5,143.11c-26.21,45.05-84,60.33-129.06,34.12-45.06-26.22-60.34-84-34.13-129.06C185.53,3.11,243.31-12.16,288.38,14.06c45.06,26.21,60.34,83.99,34.12,129.06h0Z"/>
<path class="cls-2" d="M369.41,224.99c-26.21,45.06-84,60.34-129.05,34.12-45.07-26.22-60.34-84-34.13-129.05,26.22-45.07,83.99-60.34,129.06-34.12,45.05,26.21,60.34,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M321.96,306.55c-26.22,45.06-84,60.33-129.05,34.12-45.07-26.22-60.34-83.99-34.13-129.05,26.22-45.06,83.99-60.34,129.06-34.12,45.05,26.21,60.34,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M180.66,224.39c-26.21,45.05-84,60.34-129.05,34.13C6.54,232.29-8.73,174.51,17.49,129.45c26.21-45.06,83.99-60.34,129.05-34.12,45.05,26.21,60.34,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M228.13,142.79c-26.22,45.06-84,60.34-129.06,34.12-45.07-26.22-60.34-84-34.12-129.05C91.17,2.8,148.94-12.48,194,13.74c45.06,26.21,60.34,83.99,34.13,129.05h0Z"/>
<line class="cls-6" x1="51.93" y1="258.37" x2="335.18" y2="96.19"/>
<line class="cls-6" x1="51.93" y1="95.15" x2="334.44" y2="259.11"/>
<line class="cls-5" x1="52.23" y1="258.96" x2="192.96" y2="340.57"/>
<line class="cls-5" x1="193.11" y1="340.57" x2="334.44" y2="259.41"/>
<line class="cls-5" x1="51.64" y1="258.07" x2="51.93" y2="95.15"/>
<line class="cls-5" x1="52.23" y1="95" x2="193.71" y2="13.84"/>
<line class="cls-5" x1="194.04" y1="13.73" x2="335.33" y2="96.19"/>
<polygon class="cls-10" points="99.05 340.46 4.73 177.1 99.05 13.73 287.7 13.73 382.01 177.1 287.7 340.46 99.05 340.46"/>
<line class="cls-9" x1="4.75" y1="177.1" x2="382.16" y2="177.1"/>
<line class="cls-9" x1="98.99" y1="13.76" x2="287.61" y2="340.63"/>
<line class="cls-9" x1="99.07" y1="340.46" x2="287.68" y2="13.73"/>
<line class="cls-6" x1="194" y1="13.76" x2="192.96" y2="340.65"/>
<polygon class="cls-10" points="146.23 258.65 99.08 177 146.23 95.34 240.52 95.34 287.66 177 240.52 258.65 146.23 258.65"/>
<g class="cls-12">
<path class="cls-1" d="M150.15,97.38c-1.2,2.06-3.84,2.76-5.89,1.56-2.06-1.2-2.76-3.83-1.56-5.89s3.83-2.76,5.89-1.55c2.06,1.19,2.76,3.83,1.56,5.89h0Z"/>
<path class="cls-1" d="M102.73,179.09c-1.19,2.06-3.83,2.76-5.89,1.56s-2.75-3.84-1.56-5.89c1.19-2.06,3.83-2.75,5.89-1.56,2.06,1.2,2.76,3.83,1.56,5.89Z"/>
<path class="cls-1" d="M9.08,179.3c-1.16,2.08-3.78,2.83-5.86,1.67-2.08-1.15-2.83-3.78-1.67-5.86s3.78-2.83,5.86-1.67,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M55.85,97.09c-1.16,2.08-3.78,2.83-5.86,1.67s-2.83-3.78-1.67-5.86,3.78-2.83,5.86-1.67c2.08,1.15,2.82,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M244.65,97.84c-1.2,2.06-3.83,2.75-5.89,1.56-2.06-1.2-2.76-3.84-1.56-5.89s3.84-2.75,5.89-1.56c2.06,1.2,2.75,3.83,1.56,5.89Z"/>
<path class="cls-1" d="M339.1,98.29c-1.16,2.08-3.78,2.83-5.86,1.67-2.08-1.15-2.83-3.78-1.67-5.86,1.15-2.08,3.78-2.83,5.86-1.67,2.08,1.15,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M385.86,179.3c-1.16,2.08-3.78,2.83-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.83,5.86-1.67,2.08,1.15,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M197.76,15.76c-1.16,2.08-3.78,2.83-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.82,5.86-1.67,2.08,1.16,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M291.59,15.93c-1.16,2.08-3.78,2.82-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.82,5.86-1.66,2.08,1.15,2.83,3.78,1.67,5.86h0Z"/>
<path class="cls-1" d="M102.61,15.68c-1.16,2.08-3.78,2.82-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.82,5.86-1.67s2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M236.76,256.72c1.18-2.06,3.82-2.78,5.88-1.59s2.77,3.82,1.59,5.88c-1.19,2.06-3.82,2.77-5.88,1.59-2.06-1.19-2.77-3.83-1.58-5.88h0Z"/>
<path class="cls-1" d="M189.04,338.35c1.14-2.09,3.77-2.85,5.85-1.7,2.08,1.14,2.84,3.77,1.7,5.85-1.14,2.08-3.76,2.84-5.85,1.7-2.09-1.15-2.85-3.77-1.7-5.85Z"/>
<path class="cls-1" d="M95.07,338.42c1.14-2.09,3.77-2.85,5.85-1.71,2.08,1.15,2.85,3.77,1.7,5.86-1.14,2.08-3.76,2.84-5.85,1.7-2.09-1.15-2.85-3.77-1.7-5.85Z"/>
<path class="cls-1" d="M283.61,338.42c1.14-2.09,3.77-2.85,5.85-1.71,2.09,1.15,2.85,3.77,1.7,5.86-1.14,2.08-3.77,2.84-5.85,1.7-2.09-1.15-2.85-3.77-1.7-5.85Z"/>
<path class="cls-1" d="M283.92,175.24c1.19-2.06,3.82-2.77,5.89-1.59,2.05,1.19,2.76,3.83,1.58,5.88-1.18,2.07-3.82,2.78-5.88,1.6-2.06-1.19-2.78-3.82-1.59-5.89h0Z"/>
<path class="cls-1" d="M142.25,256.71c1.19-2.07,3.82-2.77,5.89-1.59,2.06,1.2,2.77,3.83,1.58,5.89-1.18,2.06-3.82,2.77-5.88,1.58-2.06-1.18-2.77-3.82-1.58-5.88h0Z"/>
<path class="cls-1" d="M48.01,256.29c1.15-2.09,3.76-2.85,5.85-1.7,2.08,1.15,2.84,3.77,1.7,5.85s-3.77,2.84-5.85,1.69c-2.08-1.14-2.84-3.77-1.69-5.85h-.01Z"/>
<path class="cls-1" d="M330.71,257.21c1.18-2.06,3.82-2.78,5.88-1.59,2.06,1.18,2.77,3.82,1.58,5.88-1.18,2.06-3.82,2.77-5.88,1.59-2.06-1.19-2.77-3.82-1.58-5.88Z"/>
</g>
</g>
<path d="M337.96,256.21c-.69-23.4-9.19-45.58-25.33-62.76-11.71-12.47-28.78-22.47-45.42-26.33-.08,0-.16,0-.23-.02-1.28-.24-2.55-.51-3.81-.84h-.11c-.11,0-.2-.02-.29-.05-.29,0-.58-.02-.86-.09h-.3s-.07.02-.11.02c-.09,0-.17,0-.24-.03l-.91.13-.12,2.03c.91.26,1.1.43,1.37.56,2.69,1.23,6.13,2.56,8.62,4.13.14.09,2.75,1.49,3.15,1.8,3.18,2.4,3.57,3.83,6.18,7.16,4.73,6.04,8.22,13.27,10.03,20.82.27,1.11.65,8.07.58,9.44-.73,13.98-8.36,28.85-19.7,36.6-5.67,3.87-12.26,5.46-18.88,7.26,0,0-3.33.71-4.76.9-1.49.2-3.42.63-5.16.89-3.19.47-8.5,2.85-11.43,4.34-7.88,4-15.25,10.89-19.47,18.6-2.34,4.28-4.59,10.04-5.25,14.91-.07.52-.28,2.12-.3,2.58-.7,11.39,1.94,23.72,8.25,32.68,2.38,3.38,7.91,8.82,11.56,11.22,3.8,2.5,9.88,5.34,14.35,6.61.96.27,2.02.14,2.79.33.24.06.08.52.61.59,9.41,1.23,23.13-1.24,31.32-3.79,3.85-1.2,9-3.35,12.69-5.16,31.06-15.22,52.19-49.81,51.17-84.5v-.03h0ZM255.65,305.03c.27.28.38.74.15,1.09l-1.06,1.6c-.17.25-.42.4-.69.43-.11.11-.22.2-.38.25-.03,0-.06.01-.09.02l-.59,1.1c-.57.23-1.19.34-1.8.24-.15-.03-.31-.08-.45-.15-1.43-.17-2.83-1.16-3.57-2.32-2.09-3.28,2.13-8.31,4.93-7.08.02,0,.03.03.04.06h.07c.47.11.9.33,1.28.63.21.04.41.09.61.18.43.2,1.89,2.22,1.86,2.91l-.4.74c.05.1.09.2.1.3h0Z"/>
<path class="cls-11" d="M51.09,96.22c.69,23.4,9.19,45.58,25.33,62.76,11.71,12.47,28.78,22.47,45.42,26.33.08,0,.16,0,.23.02,1.28.24,2.55.51,3.81.84h.11c.11,0,.2.02.29.05.29,0,.58.02.86.09h.3s.07-.02.11-.02c.09,0,.17,0,.24.03l.91-.13.12-2.03c-.91-.26-1.1-.43-1.37-.56-2.69-1.23-6.13-2.56-8.62-4.13-.14-.09-2.75-1.49-3.15-1.8-3.18-2.4-3.57-3.83-6.18-7.16-4.73-6.04-8.22-13.27-10.03-20.82-.27-1.11-.65-8.07-.58-9.44.73-13.98,8.36-28.85,19.7-36.6,5.67-3.87,12.26-5.46,18.88-7.26,0,0,3.33-.71,4.76-.9,1.49-.2,3.42-.63,5.16-.89,3.19-.47,8.5-2.85,11.43-4.34,7.88-4,15.25-10.89,19.47-18.6,2.34-4.28,4.59-10.04,5.25-14.91.07-.52.28-2.12.3-2.58.7-11.39-1.94-23.72-8.25-32.68-2.38-3.38-7.91-8.82-11.56-11.22-3.8-2.5-9.88-5.34-14.35-6.61-.96-.27-2.02-.14-2.79-.33-.24-.06-.08-.52-.61-.59-9.41-1.23-23.13,1.24-31.32,3.79-3.85,1.2-9,3.35-12.69,5.16-31.06,15.22-52.19,49.81-51.17,84.5v.03h0ZM133.4,47.4c-.27-.28-.38-.74-.15-1.09l1.06-1.6c.17-.25.42-.4.69-.43.11-.11.22-.2.38-.25.03,0,.06,0,.09-.02l.59-1.1c.57-.23,1.19-.34,1.8-.24.15.03.31.08.45.15,1.43.17,2.83,1.16,3.57,2.32,2.09,3.28-2.13,8.31-4.93,7.08-.02,0-.03-.03-.04-.06h-.07c-.47-.11-.9-.33-1.28-.63-.21-.04-.41-.09-.61-.18-.43-.2-1.89-2.22-1.86-2.91l.4-.74c-.05-.1-.09-.2-.1-.3h0Z"/>
<circle cx="138.78" cy="47.52" r="10"/>
<circle class="cls-13" cx="251.08" cy="305.08" r="10" transform="translate(-90.28 504.05) rotate(-80.78)"/>
<g>
<g>
<path class="cls-13" d="M137.4,243.49l-30.83-94.89,80.72-58.64c3.94-2.87,9.29-2.87,13.23,0l80.72,58.64-30.83,94.89c-1.51,4.64-5.83,7.78-10.7,7.78h-91.59c-4.88,0-9.2-3.14-10.7-7.78h-.02Z"/>
<g>
<path class="cls-13" d="M195.68,85.2c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M282.89,148.58c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M249.57,251.2c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M141.64,251.24c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M108.26,148.6c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
</g>
<circle class="cls-3" cx="193.91" cy="176.2" r="5"/>
</g>
<g class="cls-15">
<path class="cls-8" d="M137.11,243.18l-30.83-94.89,80.72-58.64c3.94-2.87,9.29-2.87,13.23,0l80.72,58.64-30.83,94.89c-1.51,4.64-5.83,7.78-10.7,7.78h-91.59c-4.88,0-9.2-3.14-10.7-7.78h-.02Z"/>
<path class="cls-8" d="M187.36,238.06l-49.79-35.92c-3.95-2.85-5.62-7.93-4.13-12.57l18.78-58.46c1.49-4.64,5.8-7.8,10.68-7.81l61.41-.2c4.87-.02,9.19,3.1,10.72,7.72l19.25,58.24c1.53,4.64-.1,9.73-4.04,12.61l-49.66,36.34c-3.93,2.88-9.28,2.9-13.23.04h0Z"/>
<path class="cls-8" d="M212.51,216.61l-40.64-1.6c-4.89-.19-9.09-3.52-10.4-8.24l-10.86-39.18c-1.3-4.69.56-9.68,4.61-12.38l33.82-22.49c4.05-2.7,9.38-2.48,13.2.54l31.92,25.19c3.84,3.03,5.28,8.2,3.57,12.78l-14.24,38.07c-1.7,4.55-6.13,7.49-10.98,7.3h0Z"/>
<path class="cls-8" d="M219.56,194.36l-21.57,13.19c-4.16,2.54-9.48,2.12-13.19-1.05l-19.21-16.44c-3.7-3.17-4.95-8.36-3.08-12.87l9.7-23.35c1.87-4.5,6.43-7.29,11.29-6.9l25.21,2c4.86.39,8.92,3.86,10.05,8.6l5.88,24.59c1.13,4.74-.91,9.67-5.07,12.22h0Z"/>
<path class="cls-8" d="M214.74,176.18l-3.69,12.19c-1.41,4.67-5.67,7.89-10.54,7.99l-12.74.26c-4.87.1-9.26-2.95-10.86-7.56l-4.18-12.04c-1.6-4.61-.05-9.72,3.84-12.66l10.15-7.69c3.89-2.94,9.23-3.05,13.23-.27l10.45,7.28c4,2.79,5.75,7.83,4.34,12.5h0Z"/>
<path class="cls-8" d="M207.43,170.16l1.37,4.95c1.3,4.7-.58,9.7-4.64,12.39l-4.28,2.83c-4.07,2.69-9.41,2.45-13.22-.59l-4.02-3.2c-3.81-3.04-5.23-8.19-3.52-12.75l1.8-4.81c1.71-4.56,6.17-7.51,11.04-7.29l5.13.23c4.87.22,9.05,3.55,10.35,8.24h0Z"/>
<path class="cls-8" d="M202.16,169h0c3.28,3.61,3.87,8.92,1.46,13.15h0c-2.41,4.24-7.28,6.44-12.06,5.45h0c-4.77-.99-8.37-4.94-8.91-9.78h0c-.54-4.85,2.11-9.49,6.55-11.5h0c4.44-2.01,9.67-.93,12.96,2.68h0Z"/>
<path class="cls-7" d="M164.75,244.33l-44.12-61.11c-2.85-3.95-2.84-9.29.04-13.23l44.49-60.86c2.88-3.94,7.96-5.57,12.59-4.05l71.61,23.5c4.64,1.52,7.77,5.87,7.74,10.75l-.38,75.34c-.02,4.86-3.17,9.16-7.79,10.65l-71.6,23.13c-4.64,1.5-9.73-.16-12.58-4.12h0Z"/>
<path class="cls-7" d="M201.71,228.64l-47.54-17.41c-4.58-1.68-7.56-6.11-7.38-10.98l1.87-50.58c.18-4.87,3.48-9.07,8.17-10.41l48.71-13.86c4.68-1.33,9.68.49,12.41,4.53l28.33,41.95c2.74,4.05,2.54,9.41-.48,13.25l-31.37,39.91c-3.01,3.83-8.14,5.29-12.72,3.61h0Z"/>
<path class="cls-7" d="M217.58,205.34l-31.71,7.81c-4.76,1.17-9.74-.87-12.29-5.06l-17-27.81c-2.53-4.15-2.12-9.45,1.02-13.15l21.08-24.82c3.16-3.72,8.35-4.98,12.86-3.13l30.09,12.36c4.51,1.85,7.31,6.4,6.95,11.26l-2.46,32.46c-.37,4.85-3.81,8.91-8.53,10.08h0Z"/>
<path class="cls-7" d="M218.46,184.2l-11.67,14.58c-3.05,3.81-8.2,5.22-12.76,3.5l-17.47-6.59c-4.56-1.72-7.5-6.18-7.27-11.05l.87-18.65c.23-4.87,3.57-9.04,8.27-10.33l18.01-4.94c4.7-1.29,9.7.6,12.38,4.67l10.26,15.6c2.68,4.07,2.43,9.41-.62,13.22h0Z"/>
<path class="cls-7" d="M210.96,172.25v8.56c0,4.88-3.12,9.2-7.76,10.71l-8.13,2.65c-4.64,1.51-9.72-.13-12.59-4.08l-5.04-6.92c-2.87-3.94-2.88-9.28,0-13.23l5.02-6.93c2.86-3.95,7.94-5.6,12.58-4.1l8.14,2.63c4.64,1.5,7.78,5.82,7.79,10.69v.02h-.01Z"/>
<path class="cls-7" d="M204.48,169.3l1.13,2.01c2.39,4.25,1.78,9.56-1.53,13.14l-1.56,1.7c-3.3,3.59-8.54,4.64-12.97,2.61l-2.1-.96c-4.43-2.03-7.05-6.69-6.49-11.53l.27-2.29c.56-4.84,4.18-8.77,8.96-9.73l2.27-.46c4.78-.96,9.64,1.26,12.03,5.51h-.01Z"/>
<g>
<path class="cls-4" d="M195.39,84.89c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M282.6,148.26c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M249.28,250.89c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M141.35,250.93c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M107.97,148.29c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
</g>
</g>
</g>
</g>
<text class="cls-19" transform="translate(312.54 336.18)"><tspan class="cls-16" x="0" y="0">Je</tspan><tspan x="13.45" y="0">s</tspan><tspan class="cls-17" x="18.88" y="0">ú</tspan><tspan x="26.08" y="0">s </tspan><tspan class="cls-18" x="35.48" y="0"></tspan><tspan class="cls-14" x="48.53" y="0">rez</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="0 0 387.4 354.41">
<metadata>
<title>JPL Imago Static Signature</title>
<dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Jesús Pérez</dc:creator>
<dc:source>https://rustelo.dev</dc:source>
<dc:description>Yin-Yang symbolic figure with signature — coexistence principle</dc:description>
<dc:rights>© 2026 Jesús Pérez. All rights reserved.</dc:rights>
</metadata>
<defs>
<style>
.cls-1 {
stroke-width: 2px;
}
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9, .cls-10, .cls-11 {
stroke: #231f20;
}
.cls-1, .cls-2, .cls-10 {
fill: #e6e7e8;
}
.cls-1, .cls-4, .cls-5, .cls-6, .cls-7, .cls-9, .cls-10, .cls-11 {
stroke-miterlimit: 10;
}
.cls-2 {
stroke-width: 1.84px;
}
.cls-2, .cls-5, .cls-6, .cls-12, .cls-9, .cls-10 {
opacity: .38;
}
.cls-2, .cls-5, .cls-6, .cls-9, .cls-10 {
isolation: isolate;
}
.cls-3, .cls-13, .cls-11 {
fill: #fff;
}
.cls-4, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9 {
fill: none;
}
.cls-5 {
stroke-dasharray: 5.24 4.85;
}
.cls-14 {
letter-spacing: 0em;
}
.cls-6 {
stroke-dasharray: .84 1.39;
stroke-width: 1.11px;
}
.cls-15 {
opacity: .44;
}
.cls-7 {
stroke-dasharray: .67 1.11;
}
.cls-9, .cls-10 {
stroke-dasharray: 1.89 3.79;
stroke-width: .99px;
}
.cls-16 {
letter-spacing: 0em;
}
.cls-17 {
letter-spacing: 0em;
}
.cls-18 {
letter-spacing: -.01em;
}
.cls-19 {
font-family: SignPainter-HouseScript, SignPainter;
font-size: 18px;
}
.cls-11 {
stroke-width: .25px;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1-2">
<g>
<path class="cls-2" d="M275.05,224.67c-26.21,45.05-83.99,60.32-129.05,34.11-45.07-26.22-60.34-83.99-34.13-129.05,26.22-45.06,84-60.34,129.06-34.12,45.06,26.21,60.34,83.99,34.12,129.06Z"/>
<path class="cls-2" d="M227.58,306.26c-26.21,45.06-83.99,60.33-129.05,34.12-45.07-26.21-60.34-84-34.13-129.05,26.21-45.06,84-60.34,129.06-34.12,45.06,26.21,60.33,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M322.5,143.11c-26.21,45.05-84,60.33-129.06,34.12-45.06-26.22-60.34-84-34.13-129.06C185.53,3.11,243.31-12.16,288.38,14.06c45.06,26.21,60.34,83.99,34.12,129.06h0Z"/>
<path class="cls-2" d="M369.41,224.99c-26.21,45.06-84,60.34-129.05,34.12-45.07-26.22-60.34-84-34.13-129.05,26.22-45.07,83.99-60.34,129.06-34.12,45.05,26.21,60.34,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M321.96,306.55c-26.22,45.06-84,60.33-129.05,34.12-45.07-26.22-60.34-83.99-34.13-129.05,26.22-45.06,83.99-60.34,129.06-34.12,45.05,26.21,60.34,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M180.66,224.39c-26.21,45.05-84,60.34-129.05,34.13C6.54,232.29-8.73,174.51,17.49,129.45c26.21-45.06,83.99-60.34,129.05-34.12,45.05,26.21,60.34,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M228.13,142.79c-26.22,45.06-84,60.34-129.06,34.12-45.07-26.22-60.34-84-34.12-129.05C91.17,2.8,148.94-12.48,194,13.74c45.06,26.21,60.34,83.99,34.13,129.05h0Z"/>
<line class="cls-6" x1="51.93" y1="258.37" x2="335.18" y2="96.19"/>
<line class="cls-6" x1="51.93" y1="95.15" x2="334.44" y2="259.11"/>
<line class="cls-5" x1="52.23" y1="258.96" x2="192.96" y2="340.57"/>
<line class="cls-5" x1="193.11" y1="340.57" x2="334.44" y2="259.41"/>
<line class="cls-5" x1="51.64" y1="258.07" x2="51.93" y2="95.15"/>
<line class="cls-5" x1="52.23" y1="95" x2="193.71" y2="13.84"/>
<line class="cls-5" x1="194.04" y1="13.73" x2="335.33" y2="96.19"/>
<polygon class="cls-10" points="99.05 340.46 4.73 177.1 99.05 13.73 287.7 13.73 382.01 177.1 287.7 340.46 99.05 340.46"/>
<line class="cls-9" x1="4.75" y1="177.1" x2="382.16" y2="177.1"/>
<line class="cls-9" x1="98.99" y1="13.76" x2="287.61" y2="340.63"/>
<line class="cls-9" x1="99.07" y1="340.46" x2="287.68" y2="13.73"/>
<line class="cls-6" x1="194" y1="13.76" x2="192.96" y2="340.65"/>
<polygon class="cls-10" points="146.23 258.65 99.08 177 146.23 95.34 240.52 95.34 287.66 177 240.52 258.65 146.23 258.65"/>
<g class="cls-12">
<path class="cls-1" d="M150.15,97.38c-1.2,2.06-3.84,2.76-5.89,1.56-2.06-1.2-2.76-3.83-1.56-5.89s3.83-2.76,5.89-1.55c2.06,1.19,2.76,3.83,1.56,5.89h0Z"/>
<path class="cls-1" d="M102.73,179.09c-1.19,2.06-3.83,2.76-5.89,1.56s-2.75-3.84-1.56-5.89c1.19-2.06,3.83-2.75,5.89-1.56,2.06,1.2,2.76,3.83,1.56,5.89Z"/>
<path class="cls-1" d="M9.08,179.3c-1.16,2.08-3.78,2.83-5.86,1.67-2.08-1.15-2.83-3.78-1.67-5.86s3.78-2.83,5.86-1.67,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M55.85,97.09c-1.16,2.08-3.78,2.83-5.86,1.67s-2.83-3.78-1.67-5.86,3.78-2.83,5.86-1.67c2.08,1.15,2.82,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M244.65,97.84c-1.2,2.06-3.83,2.75-5.89,1.56-2.06-1.2-2.76-3.84-1.56-5.89s3.84-2.75,5.89-1.56c2.06,1.2,2.75,3.83,1.56,5.89Z"/>
<path class="cls-1" d="M339.1,98.29c-1.16,2.08-3.78,2.83-5.86,1.67-2.08-1.15-2.83-3.78-1.67-5.86,1.15-2.08,3.78-2.83,5.86-1.67,2.08,1.15,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M385.86,179.3c-1.16,2.08-3.78,2.83-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.83,5.86-1.67,2.08,1.15,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M197.76,15.76c-1.16,2.08-3.78,2.83-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.82,5.86-1.67,2.08,1.16,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M291.59,15.93c-1.16,2.08-3.78,2.82-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.82,5.86-1.66,2.08,1.15,2.83,3.78,1.67,5.86h0Z"/>
<path class="cls-1" d="M102.61,15.68c-1.16,2.08-3.78,2.82-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.82,5.86-1.67s2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M236.76,256.72c1.18-2.06,3.82-2.78,5.88-1.59s2.77,3.82,1.59,5.88c-1.19,2.06-3.82,2.77-5.88,1.59-2.06-1.19-2.77-3.83-1.58-5.88h0Z"/>
<path class="cls-1" d="M189.04,338.35c1.14-2.09,3.77-2.85,5.85-1.7,2.08,1.14,2.84,3.77,1.7,5.85-1.14,2.08-3.76,2.84-5.85,1.7-2.09-1.15-2.85-3.77-1.7-5.85Z"/>
<path class="cls-1" d="M95.07,338.42c1.14-2.09,3.77-2.85,5.85-1.71,2.08,1.15,2.85,3.77,1.7,5.86-1.14,2.08-3.76,2.84-5.85,1.7-2.09-1.15-2.85-3.77-1.7-5.85Z"/>
<path class="cls-1" d="M283.61,338.42c1.14-2.09,3.77-2.85,5.85-1.71,2.09,1.15,2.85,3.77,1.7,5.86-1.14,2.08-3.77,2.84-5.85,1.7-2.09-1.15-2.85-3.77-1.7-5.85Z"/>
<path class="cls-1" d="M283.92,175.24c1.19-2.06,3.82-2.77,5.89-1.59,2.05,1.19,2.76,3.83,1.58,5.88-1.18,2.07-3.82,2.78-5.88,1.6-2.06-1.19-2.78-3.82-1.59-5.89h0Z"/>
<path class="cls-1" d="M142.25,256.71c1.19-2.07,3.82-2.77,5.89-1.59,2.06,1.2,2.77,3.83,1.58,5.89-1.18,2.06-3.82,2.77-5.88,1.58-2.06-1.18-2.77-3.82-1.58-5.88h0Z"/>
<path class="cls-1" d="M48.01,256.29c1.15-2.09,3.76-2.85,5.85-1.7,2.08,1.15,2.84,3.77,1.7,5.85s-3.77,2.84-5.85,1.69c-2.08-1.14-2.84-3.77-1.69-5.85h-.01Z"/>
<path class="cls-1" d="M330.71,257.21c1.18-2.06,3.82-2.78,5.88-1.59,2.06,1.18,2.77,3.82,1.58,5.88-1.18,2.06-3.82,2.77-5.88,1.59-2.06-1.19-2.77-3.82-1.58-5.88Z"/>
</g>
</g>
<path d="M337.96,256.21c-.69-23.4-9.19-45.58-25.33-62.76-11.71-12.47-28.78-22.47-45.42-26.33-.08,0-.16,0-.23-.02-1.28-.24-2.55-.51-3.81-.84h-.11c-.11,0-.2-.02-.29-.05-.29,0-.58-.02-.86-.09h-.3s-.07.02-.11.02c-.09,0-.17,0-.24-.03l-.91.13-.12,2.03c.91.26,1.1.43,1.37.56,2.69,1.23,6.13,2.56,8.62,4.13.14.09,2.75,1.49,3.15,1.8,3.18,2.4,3.57,3.83,6.18,7.16,4.73,6.04,8.22,13.27,10.03,20.82.27,1.11.65,8.07.58,9.44-.73,13.98-8.36,28.85-19.7,36.6-5.67,3.87-12.26,5.46-18.88,7.26,0,0-3.33.71-4.76.9-1.49.2-3.42.63-5.16.89-3.19.47-8.5,2.85-11.43,4.34-7.88,4-15.25,10.89-19.47,18.6-2.34,4.28-4.59,10.04-5.25,14.91-.07.52-.28,2.12-.3,2.58-.7,11.39,1.94,23.72,8.25,32.68,2.38,3.38,7.91,8.82,11.56,11.22,3.8,2.5,9.88,5.34,14.35,6.61.96.27,2.02.14,2.79.33.24.06.08.52.61.59,9.41,1.23,23.13-1.24,31.32-3.79,3.85-1.2,9-3.35,12.69-5.16,31.06-15.22,52.19-49.81,51.17-84.5v-.03h0ZM255.65,305.03c.27.28.38.74.15,1.09l-1.06,1.6c-.17.25-.42.4-.69.43-.11.11-.22.2-.38.25-.03,0-.06.01-.09.02l-.59,1.1c-.57.23-1.19.34-1.8.24-.15-.03-.31-.08-.45-.15-1.43-.17-2.83-1.16-3.57-2.32-2.09-3.28,2.13-8.31,4.93-7.08.02,0,.03.03.04.06h.07c.47.11.9.33,1.28.63.21.04.41.09.61.18.43.2,1.89,2.22,1.86,2.91l-.4.74c.05.1.09.2.1.3h0Z"/>
<path class="cls-11" d="M51.09,96.22c.69,23.4,9.19,45.58,25.33,62.76,11.71,12.47,28.78,22.47,45.42,26.33.08,0,.16,0,.23.02,1.28.24,2.55.51,3.81.84h.11c.11,0,.2.02.29.05.29,0,.58.02.86.09h.3s.07-.02.11-.02c.09,0,.17,0,.24.03l.91-.13.12-2.03c-.91-.26-1.1-.43-1.37-.56-2.69-1.23-6.13-2.56-8.62-4.13-.14-.09-2.75-1.49-3.15-1.8-3.18-2.4-3.57-3.83-6.18-7.16-4.73-6.04-8.22-13.27-10.03-20.82-.27-1.11-.65-8.07-.58-9.44.73-13.98,8.36-28.85,19.7-36.6,5.67-3.87,12.26-5.46,18.88-7.26,0,0,3.33-.71,4.76-.9,1.49-.2,3.42-.63,5.16-.89,3.19-.47,8.5-2.85,11.43-4.34,7.88-4,15.25-10.89,19.47-18.6,2.34-4.28,4.59-10.04,5.25-14.91.07-.52.28-2.12.3-2.58.7-11.39-1.94-23.72-8.25-32.68-2.38-3.38-7.91-8.82-11.56-11.22-3.8-2.5-9.88-5.34-14.35-6.61-.96-.27-2.02-.14-2.79-.33-.24-.06-.08-.52-.61-.59-9.41-1.23-23.13,1.24-31.32,3.79-3.85,1.2-9,3.35-12.69,5.16-31.06,15.22-52.19,49.81-51.17,84.5v.03h0ZM133.4,47.4c-.27-.28-.38-.74-.15-1.09l1.06-1.6c.17-.25.42-.4.69-.43.11-.11.22-.2.38-.25.03,0,.06,0,.09-.02l.59-1.1c.57-.23,1.19-.34,1.8-.24.15.03.31.08.45.15,1.43.17,2.83,1.16,3.57,2.32,2.09,3.28-2.13,8.31-4.93,7.08-.02,0-.03-.03-.04-.06h-.07c-.47-.11-.9-.33-1.28-.63-.21-.04-.41-.09-.61-.18-.43-.2-1.89-2.22-1.86-2.91l.4-.74c-.05-.1-.09-.2-.1-.3h0Z"/>
<circle cx="138.78" cy="47.52" r="10"/>
<circle class="cls-13" cx="251.08" cy="305.08" r="10" transform="translate(-90.28 504.05) rotate(-80.78)"/>
<g>
<g>
<path class="cls-13" d="M137.4,243.49l-30.83-94.89,80.72-58.64c3.94-2.87,9.29-2.87,13.23,0l80.72,58.64-30.83,94.89c-1.51,4.64-5.83,7.78-10.7,7.78h-91.59c-4.88,0-9.2-3.14-10.7-7.78h-.02Z"/>
<g>
<path class="cls-13" d="M195.68,85.2c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M282.89,148.58c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M249.57,251.2c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M141.64,251.24c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M108.26,148.6c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
</g>
<circle class="cls-3" cx="193.91" cy="176.2" r="5"/>
</g>
<g class="cls-15">
<path class="cls-8" d="M137.11,243.18l-30.83-94.89,80.72-58.64c3.94-2.87,9.29-2.87,13.23,0l80.72,58.64-30.83,94.89c-1.51,4.64-5.83,7.78-10.7,7.78h-91.59c-4.88,0-9.2-3.14-10.7-7.78h-.02Z"/>
<path class="cls-8" d="M187.36,238.06l-49.79-35.92c-3.95-2.85-5.62-7.93-4.13-12.57l18.78-58.46c1.49-4.64,5.8-7.8,10.68-7.81l61.41-.2c4.87-.02,9.19,3.1,10.72,7.72l19.25,58.24c1.53,4.64-.1,9.73-4.04,12.61l-49.66,36.34c-3.93,2.88-9.28,2.9-13.23.04h0Z"/>
<path class="cls-8" d="M212.51,216.61l-40.64-1.6c-4.89-.19-9.09-3.52-10.4-8.24l-10.86-39.18c-1.3-4.69.56-9.68,4.61-12.38l33.82-22.49c4.05-2.7,9.38-2.48,13.2.54l31.92,25.19c3.84,3.03,5.28,8.2,3.57,12.78l-14.24,38.07c-1.7,4.55-6.13,7.49-10.98,7.3h0Z"/>
<path class="cls-8" d="M219.56,194.36l-21.57,13.19c-4.16,2.54-9.48,2.12-13.19-1.05l-19.21-16.44c-3.7-3.17-4.95-8.36-3.08-12.87l9.7-23.35c1.87-4.5,6.43-7.29,11.29-6.9l25.21,2c4.86.39,8.92,3.86,10.05,8.6l5.88,24.59c1.13,4.74-.91,9.67-5.07,12.22h0Z"/>
<path class="cls-8" d="M214.74,176.18l-3.69,12.19c-1.41,4.67-5.67,7.89-10.54,7.99l-12.74.26c-4.87.1-9.26-2.95-10.86-7.56l-4.18-12.04c-1.6-4.61-.05-9.72,3.84-12.66l10.15-7.69c3.89-2.94,9.23-3.05,13.23-.27l10.45,7.28c4,2.79,5.75,7.83,4.34,12.5h0Z"/>
<path class="cls-8" d="M207.43,170.16l1.37,4.95c1.3,4.7-.58,9.7-4.64,12.39l-4.28,2.83c-4.07,2.69-9.41,2.45-13.22-.59l-4.02-3.2c-3.81-3.04-5.23-8.19-3.52-12.75l1.8-4.81c1.71-4.56,6.17-7.51,11.04-7.29l5.13.23c4.87.22,9.05,3.55,10.35,8.24h0Z"/>
<path class="cls-8" d="M202.16,169h0c3.28,3.61,3.87,8.92,1.46,13.15h0c-2.41,4.24-7.28,6.44-12.06,5.45h0c-4.77-.99-8.37-4.94-8.91-9.78h0c-.54-4.85,2.11-9.49,6.55-11.5h0c4.44-2.01,9.67-.93,12.96,2.68h0Z"/>
<path class="cls-7" d="M164.75,244.33l-44.12-61.11c-2.85-3.95-2.84-9.29.04-13.23l44.49-60.86c2.88-3.94,7.96-5.57,12.59-4.05l71.61,23.5c4.64,1.52,7.77,5.87,7.74,10.75l-.38,75.34c-.02,4.86-3.17,9.16-7.79,10.65l-71.6,23.13c-4.64,1.5-9.73-.16-12.58-4.12h0Z"/>
<path class="cls-7" d="M201.71,228.64l-47.54-17.41c-4.58-1.68-7.56-6.11-7.38-10.98l1.87-50.58c.18-4.87,3.48-9.07,8.17-10.41l48.71-13.86c4.68-1.33,9.68.49,12.41,4.53l28.33,41.95c2.74,4.05,2.54,9.41-.48,13.25l-31.37,39.91c-3.01,3.83-8.14,5.29-12.72,3.61h0Z"/>
<path class="cls-7" d="M217.58,205.34l-31.71,7.81c-4.76,1.17-9.74-.87-12.29-5.06l-17-27.81c-2.53-4.15-2.12-9.45,1.02-13.15l21.08-24.82c3.16-3.72,8.35-4.98,12.86-3.13l30.09,12.36c4.51,1.85,7.31,6.4,6.95,11.26l-2.46,32.46c-.37,4.85-3.81,8.91-8.53,10.08h0Z"/>
<path class="cls-7" d="M218.46,184.2l-11.67,14.58c-3.05,3.81-8.2,5.22-12.76,3.5l-17.47-6.59c-4.56-1.72-7.5-6.18-7.27-11.05l.87-18.65c.23-4.87,3.57-9.04,8.27-10.33l18.01-4.94c4.7-1.29,9.7.6,12.38,4.67l10.26,15.6c2.68,4.07,2.43,9.41-.62,13.22h0Z"/>
<path class="cls-7" d="M210.96,172.25v8.56c0,4.88-3.12,9.2-7.76,10.71l-8.13,2.65c-4.64,1.51-9.72-.13-12.59-4.08l-5.04-6.92c-2.87-3.94-2.88-9.28,0-13.23l5.02-6.93c2.86-3.95,7.94-5.6,12.58-4.1l8.14,2.63c4.64,1.5,7.78,5.82,7.79,10.69v.02h-.01Z"/>
<path class="cls-7" d="M204.48,169.3l1.13,2.01c2.39,4.25,1.78,9.56-1.53,13.14l-1.56,1.7c-3.3,3.59-8.54,4.64-12.97,2.61l-2.1-.96c-4.43-2.03-7.05-6.69-6.49-11.53l.27-2.29c.56-4.84,4.18-8.77,8.96-9.73l2.27-.46c4.78-.96,9.64,1.26,12.03,5.51h-.01Z"/>
<g>
<path class="cls-4" d="M195.39,84.89c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M282.6,148.26c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M249.28,250.89c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M141.35,250.93c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M107.97,148.29c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
</g>
</g>
</g>
</g>
<text class="cls-19" transform="translate(312.54 336.18)"><tspan class="cls-16" x="0" y="0">Je</tspan><tspan x="13.45" y="0">s</tspan><tspan class="cls-17" x="18.88" y="0">ú</tspan><tspan x="26.08" y="0">s </tspan><tspan class="cls-18" x="35.48" y="0"></tspan><tspan class="cls-14" x="48.53" y="0">rez</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 387.4 354.41">
<defs>
<style>
.cls-1 {
stroke-width: 2px;
}
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9, .cls-10, .cls-11 {
stroke: #231f20;
}
.cls-1, .cls-2, .cls-10 {
fill: #e6e7e8;
}
.cls-1, .cls-4, .cls-5, .cls-6, .cls-7, .cls-9, .cls-10, .cls-11 {
stroke-miterlimit: 10;
}
.cls-2 {
stroke-width: 1.84px;
}
.cls-2, .cls-5, .cls-6, .cls-12, .cls-9, .cls-10 {
opacity: .38;
}
.cls-2, .cls-5, .cls-6, .cls-9, .cls-10 {
isolation: isolate;
}
.cls-3, .cls-13, .cls-11 {
fill: #fff;
}
.cls-4, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9 {
fill: none;
}
.cls-5 {
stroke-dasharray: 5.24 4.85;
}
.cls-6 {
stroke-dasharray: .84 1.39;
stroke-width: 1.11px;
}
.cls-14 {
opacity: .44;
}
.cls-7 {
stroke-dasharray: .67 1.11;
}
.cls-9, .cls-10 {
stroke-dasharray: 1.89 3.79;
stroke-width: .99px;
}
.cls-11 {
stroke-width: .25px;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<g>
<path class="cls-2" d="M275.05,224.67c-26.21,45.05-83.99,60.32-129.05,34.11-45.07-26.22-60.34-83.99-34.13-129.05,26.22-45.06,84-60.34,129.06-34.12,45.06,26.21,60.34,83.99,34.12,129.06Z"/>
<path class="cls-2" d="M227.58,306.26c-26.21,45.06-83.99,60.33-129.05,34.12-45.07-26.21-60.34-84-34.13-129.05,26.21-45.06,84-60.34,129.06-34.12,45.06,26.21,60.33,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M322.5,143.11c-26.21,45.05-84,60.33-129.06,34.12-45.06-26.22-60.34-84-34.13-129.06C185.53,3.11,243.31-12.16,288.38,14.06c45.06,26.21,60.34,83.99,34.12,129.06h0Z"/>
<path class="cls-2" d="M369.41,224.99c-26.21,45.06-84,60.34-129.05,34.12-45.07-26.22-60.34-84-34.13-129.05,26.22-45.07,83.99-60.34,129.06-34.12,45.05,26.21,60.34,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M321.96,306.55c-26.22,45.06-84,60.33-129.05,34.12-45.07-26.22-60.34-83.99-34.13-129.05,26.22-45.06,83.99-60.34,129.06-34.12,45.05,26.21,60.34,83.99,34.12,129.05Z"/>
<path class="cls-2" d="M180.66,224.39c-26.21,45.05-84,60.34-129.05,34.13C6.54,232.29-8.73,174.51,17.49,129.45c26.21-45.06,83.99-60.34,129.05-34.12,45.05,26.21,60.34,83.99,34.12,129.05h0Z"/>
<path class="cls-2" d="M228.13,142.79c-26.22,45.06-84,60.34-129.06,34.12-45.07-26.22-60.34-84-34.12-129.05C91.17,2.8,148.94-12.48,194,13.74c45.06,26.21,60.34,83.99,34.13,129.05h0Z"/>
<line class="cls-6" x1="51.93" y1="258.37" x2="335.18" y2="96.19"/>
<line class="cls-6" x1="51.93" y1="95.15" x2="334.44" y2="259.11"/>
<line class="cls-5" x1="52.23" y1="258.96" x2="192.96" y2="340.57"/>
<line class="cls-5" x1="193.11" y1="340.57" x2="334.44" y2="259.41"/>
<line class="cls-5" x1="51.64" y1="258.07" x2="51.93" y2="95.15"/>
<line class="cls-5" x1="52.23" y1="95" x2="193.71" y2="13.84"/>
<line class="cls-5" x1="194.04" y1="13.73" x2="335.33" y2="96.19"/>
<polygon class="cls-10" points="99.05 340.46 4.73 177.1 99.05 13.73 287.7 13.73 382.01 177.1 287.7 340.46 99.05 340.46"/>
<line class="cls-9" x1="4.75" y1="177.1" x2="382.16" y2="177.1"/>
<line class="cls-9" x1="98.99" y1="13.76" x2="287.61" y2="340.63"/>
<line class="cls-9" x1="99.07" y1="340.46" x2="287.68" y2="13.73"/>
<line class="cls-6" x1="194" y1="13.76" x2="192.96" y2="340.65"/>
<polygon class="cls-10" points="146.23 258.65 99.08 177 146.23 95.34 240.52 95.34 287.66 177 240.52 258.65 146.23 258.65"/>
<g class="cls-12">
<path class="cls-1" d="M150.15,97.38c-1.2,2.06-3.84,2.76-5.89,1.56-2.06-1.2-2.76-3.83-1.56-5.89s3.83-2.76,5.89-1.55c2.06,1.19,2.76,3.83,1.56,5.89h0Z"/>
<path class="cls-1" d="M102.73,179.09c-1.19,2.06-3.83,2.76-5.89,1.56s-2.75-3.84-1.56-5.89c1.19-2.06,3.83-2.75,5.89-1.56,2.06,1.2,2.76,3.83,1.56,5.89Z"/>
<path class="cls-1" d="M9.08,179.3c-1.16,2.08-3.78,2.83-5.86,1.67-2.08-1.15-2.83-3.78-1.67-5.86s3.78-2.83,5.86-1.67,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M55.85,97.09c-1.16,2.08-3.78,2.83-5.86,1.67-2.08-1.16-2.83-3.78-1.67-5.86s3.78-2.83,5.86-1.67c2.08,1.15,2.82,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M244.65,97.84c-1.2,2.06-3.83,2.75-5.89,1.56-2.06-1.2-2.76-3.84-1.56-5.89s3.84-2.75,5.89-1.56c2.06,1.2,2.75,3.83,1.56,5.89Z"/>
<path class="cls-1" d="M339.1,98.29c-1.16,2.08-3.78,2.83-5.86,1.67-2.08-1.15-2.83-3.78-1.67-5.86,1.15-2.08,3.78-2.83,5.86-1.67,2.08,1.15,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M385.86,179.3c-1.16,2.08-3.78,2.83-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.83,5.86-1.67,2.08,1.15,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M197.76,15.76c-1.16,2.08-3.78,2.83-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.82,5.86-1.67,2.08,1.16,2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M291.59,15.93c-1.16,2.08-3.78,2.82-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.82,5.86-1.66,2.08,1.15,2.83,3.78,1.67,5.86h0Z"/>
<path class="cls-1" d="M102.61,15.68c-1.16,2.08-3.78,2.82-5.86,1.67s-2.83-3.78-1.67-5.86c1.15-2.08,3.78-2.82,5.86-1.67s2.83,3.78,1.67,5.86Z"/>
<path class="cls-1" d="M236.76,256.72c1.18-2.06,3.82-2.78,5.88-1.59s2.77,3.82,1.59,5.88c-1.19,2.06-3.82,2.77-5.88,1.59-2.06-1.19-2.77-3.83-1.58-5.88h0Z"/>
<path class="cls-1" d="M189.04,338.35c1.14-2.09,3.77-2.85,5.85-1.7,2.08,1.14,2.84,3.77,1.7,5.85-1.14,2.08-3.76,2.84-5.85,1.7-2.09-1.15-2.85-3.77-1.7-5.85Z"/>
<path class="cls-1" d="M95.07,338.42c1.14-2.09,3.77-2.85,5.85-1.71,2.08,1.15,2.85,3.77,1.7,5.86-1.14,2.08-3.76,2.84-5.85,1.7-2.09-1.15-2.85-3.77-1.7-5.85Z"/>
<path class="cls-1" d="M283.61,338.42c1.14-2.09,3.77-2.85,5.85-1.71,2.09,1.15,2.85,3.77,1.7,5.86-1.14,2.08-3.77,2.84-5.85,1.7-2.09-1.15-2.85-3.77-1.7-5.85Z"/>
<path class="cls-1" d="M283.92,175.24c1.19-2.06,3.82-2.77,5.89-1.59,2.05,1.19,2.76,3.83,1.58,5.88-1.18,2.07-3.82,2.78-5.88,1.6-2.06-1.19-2.78-3.82-1.59-5.89h0Z"/>
<path class="cls-1" d="M142.25,256.71c1.19-2.07,3.82-2.77,5.89-1.59,2.06,1.2,2.77,3.83,1.58,5.89-1.18,2.06-3.82,2.77-5.88,1.58-2.06-1.18-2.77-3.82-1.58-5.88h0Z"/>
<path class="cls-1" d="M48.01,256.29c1.15-2.09,3.76-2.85,5.85-1.7,2.08,1.15,2.84,3.77,1.7,5.85s-3.77,2.84-5.85,1.69c-2.08-1.14-2.84-3.77-1.69-5.85h-.01Z"/>
<path class="cls-1" d="M330.71,257.21c1.18-2.06,3.82-2.78,5.88-1.59,2.06,1.18,2.77,3.82,1.58,5.88-1.18,2.06-3.82,2.77-5.88,1.59-2.06-1.19-2.77-3.82-1.58-5.88Z"/>
</g>
</g>
<path d="M337.96,256.21c-.69-23.4-9.19-45.58-25.33-62.76-11.71-12.47-28.78-22.47-45.42-26.33-.08,0-.16,0-.23-.02-1.28-.24-2.55-.51-3.81-.84-.04,0-.07,0-.11,0-.11,0-.2-.02-.29-.05-.29,0-.58-.02-.86-.09h-.21s-.06,0-.09,0c-.04,0-.07.02-.11.02-.09,0-.17,0-.24-.03l-.91.13-.12,2.03c.91.26,1.1.43,1.37.56,2.69,1.23,6.13,2.56,8.62,4.13.14.09,2.75,1.49,3.15,1.8,3.18,2.4,3.57,3.83,6.18,7.16,4.73,6.04,8.22,13.27,10.03,20.82.27,1.11.65,8.07.58,9.44-.73,13.98-8.36,28.85-19.7,36.6-5.67,3.87-12.26,5.46-18.88,7.26,0,0-3.33.71-4.76.9-1.49.2-3.42.63-5.16.89-3.19.47-8.5,2.85-11.43,4.34-7.88,4-15.25,10.89-19.47,18.6-2.34,4.28-4.59,10.04-5.25,14.91-.07.52-.28,2.12-.3,2.58-.7,11.39,1.94,23.72,8.25,32.68,2.38,3.38,7.91,8.82,11.56,11.22,3.8,2.5,9.88,5.34,14.35,6.61.96.27,2.02.14,2.79.33.24.06.08.52.61.59,9.41,1.23,23.13-1.24,31.32-3.79,3.85-1.2,9-3.35,12.69-5.16,31.06-15.22,52.19-49.81,51.17-84.5v-.03ZM255.65,305.03c.27.28.38.74.15,1.09l-1.06,1.6c-.17.25-.42.4-.69.43-.11.11-.22.2-.38.25-.03,0-.06.01-.09.02l-.59,1.1c-.57.23-1.19.34-1.8.24-.15-.03-.31-.08-.45-.15-1.43-.17-2.83-1.16-3.57-2.32-2.09-3.28,2.13-8.31,4.93-7.08.02,0,.03.03.04.06h.07c.47.11.9.33,1.28.63.21.04.41.09.61.18.43.2,1.89,2.22,1.86,2.91l-.4.74c.05.1.09.2.1.3h0Z"/>
<path class="cls-11" d="M51.09,96.22c.69,23.4,9.19,45.58,25.33,62.76,11.71,12.47,28.78,22.47,45.42,26.33.08,0,.16,0,.23.02,1.28.24,2.55.51,3.81.84.04,0,.07,0,.11,0,.11,0,.2.02.29.05.29,0,.58.02.86.09h.21s.06,0,.09,0c.04,0,.07-.02.11-.02.09,0,.17,0,.24.03l.91-.13.12-2.03c-.91-.26-1.1-.43-1.37-.56-2.69-1.23-6.13-2.56-8.62-4.13-.14-.09-2.75-1.49-3.15-1.8-3.18-2.4-3.57-3.83-6.18-7.16-4.73-6.04-8.22-13.27-10.03-20.82-.27-1.11-.65-8.07-.58-9.44.73-13.98,8.36-28.85,19.7-36.6,5.67-3.87,12.26-5.46,18.88-7.26,0,0,3.33-.71,4.76-.9,1.49-.2,3.42-.63,5.16-.89,3.19-.47,8.5-2.85,11.43-4.34,7.88-4,15.25-10.89,19.47-18.6,2.34-4.28,4.59-10.04,5.25-14.91.07-.52.28-2.12.3-2.58.7-11.39-1.94-23.72-8.25-32.68-2.38-3.38-7.91-8.82-11.56-11.22-3.8-2.5-9.88-5.34-14.35-6.61-.96-.27-2.02-.14-2.79-.33-.24-.06-.08-.52-.61-.59-9.41-1.23-23.13,1.24-31.32,3.79-3.85,1.2-9,3.35-12.69,5.16-31.06,15.22-52.19,49.81-51.17,84.5v.03ZM133.4,47.4c-.27-.28-.38-.74-.15-1.09l1.06-1.6c.17-.25.42-.4.69-.43.11-.11.22-.2.38-.25.03,0,.06,0,.09-.02l.59-1.1c.57-.23,1.19-.34,1.8-.24.15.03.31.08.45.15,1.43.17,2.83,1.16,3.57,2.32,2.09,3.28-2.13,8.31-4.93,7.08-.02,0-.03-.03-.04-.06h-.07c-.47-.11-.9-.33-1.28-.63-.21-.04-.41-.09-.61-.18-.43-.2-1.89-2.22-1.86-2.91l.4-.74c-.05-.1-.09-.2-.1-.3h0Z"/>
<circle cx="138.78" cy="47.52" r="10"/>
<circle class="cls-13" cx="251.08" cy="305.08" r="10"/>
<g>
<g>
<path class="cls-13" d="M137.4,243.49l-30.83-94.89,80.72-58.64c3.94-2.87,9.29-2.87,13.23,0l80.72,58.64-30.83,94.89c-1.51,4.64-5.83,7.78-10.7,7.78h-91.59c-4.88,0-9.2-3.14-10.7-7.78h-.02Z"/>
<g>
<path class="cls-13" d="M195.68,85.2c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M282.89,148.58c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M249.57,251.2c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M141.64,251.24c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-13" d="M108.26,148.6c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
</g>
<circle class="cls-3" cx="193.91" cy="176.2" r="5"/>
</g>
<g class="cls-14">
<path class="cls-8" d="M137.11,243.18l-30.83-94.89,80.72-58.64c3.94-2.87,9.29-2.87,13.23,0l80.72,58.64-30.83,94.89c-1.51,4.64-5.83,7.78-10.7,7.78h-91.59c-4.88,0-9.2-3.14-10.7-7.78h-.02Z"/>
<path class="cls-8" d="M187.36,238.06l-49.79-35.92c-3.95-2.85-5.62-7.93-4.13-12.57l18.78-58.46c1.49-4.64,5.8-7.8,10.68-7.81l61.41-.2c4.87-.02,9.19,3.1,10.72,7.72l19.25,58.24c1.53,4.64-.1,9.73-4.04,12.61l-49.66,36.34c-3.93,2.88-9.28,2.9-13.23.04h.01Z"/>
<path class="cls-8" d="M212.51,216.61l-40.64-1.6c-4.89-.19-9.09-3.52-10.4-8.24l-10.86-39.18c-1.3-4.69.56-9.68,4.61-12.38l33.82-22.49c4.05-2.7,9.38-2.48,13.2.54l31.92,25.19c3.84,3.03,5.28,8.2,3.57,12.78l-14.24,38.07c-1.7,4.55-6.13,7.49-10.98,7.3h0Z"/>
<path class="cls-8" d="M219.56,194.36l-21.57,13.19c-4.16,2.54-9.48,2.12-13.19-1.05l-19.21-16.44c-3.7-3.17-4.95-8.36-3.08-12.87l9.7-23.35c1.87-4.5,6.43-7.29,11.29-6.9l25.21,2c4.86.39,8.92,3.86,10.05,8.6l5.88,24.59c1.13,4.74-.91,9.67-5.07,12.22h-.01Z"/>
<path class="cls-8" d="M214.74,176.18l-3.69,12.19c-1.41,4.67-5.67,7.89-10.54,7.99l-12.74.26c-4.87.1-9.26-2.95-10.86-7.56l-4.18-12.04c-1.6-4.61-.05-9.72,3.84-12.66l10.15-7.69c3.89-2.94,9.23-3.05,13.23-.27l10.45,7.28c4,2.79,5.75,7.83,4.34,12.5h0Z"/>
<path class="cls-8" d="M207.43,170.16l1.37,4.95c1.3,4.7-.58,9.7-4.64,12.39l-4.28,2.83c-4.07,2.69-9.41,2.45-13.22-.59l-4.02-3.2c-3.81-3.04-5.23-8.19-3.52-12.75l1.8-4.81c1.71-4.56,6.17-7.51,11.04-7.29l5.13.23c4.87.22,9.05,3.55,10.35,8.24h0Z"/>
<path class="cls-8" d="M202.16,169h0c3.28,3.61,3.87,8.92,1.46,13.15h0c-2.41,4.24-7.28,6.44-12.06,5.45h0c-4.77-.99-8.37-4.94-8.91-9.78h0c-.54-4.85,2.11-9.49,6.55-11.5h0c4.44-2.01,9.67-.93,12.96,2.68h0Z"/>
<path class="cls-7" d="M164.75,244.33l-44.12-61.11c-2.85-3.95-2.84-9.29.04-13.23l44.49-60.86c2.88-3.94,7.96-5.57,12.59-4.05l71.61,23.5c4.64,1.52,7.77,5.87,7.74,10.75l-.38,75.34c-.02,4.86-3.17,9.16-7.79,10.65l-71.6,23.13c-4.64,1.5-9.73-.16-12.58-4.12Z"/>
<path class="cls-7" d="M201.71,228.64l-47.54-17.41c-4.58-1.68-7.56-6.11-7.38-10.98l1.87-50.58c.18-4.87,3.48-9.07,8.17-10.41l48.71-13.86c4.68-1.33,9.68.49,12.41,4.53l28.33,41.95c2.74,4.05,2.54,9.41-.48,13.25l-31.37,39.91c-3.01,3.83-8.14,5.29-12.72,3.61h0Z"/>
<path class="cls-7" d="M217.58,205.34l-31.71,7.81c-4.76,1.17-9.74-.87-12.29-5.06l-17-27.81c-2.53-4.15-2.12-9.45,1.02-13.15l21.08-24.82c3.16-3.72,8.35-4.98,12.86-3.13l30.09,12.36c4.51,1.85,7.31,6.4,6.95,11.26l-2.46,32.46c-.37,4.85-3.81,8.91-8.53,10.08h-.01Z"/>
<path class="cls-7" d="M218.46,184.2l-11.67,14.58c-3.05,3.81-8.2,5.22-12.76,3.5l-17.47-6.59c-4.56-1.72-7.5-6.18-7.27-11.05l.87-18.65c.23-4.87,3.57-9.04,8.27-10.33l18.01-4.94c4.7-1.29,9.7.6,12.38,4.67l10.26,15.6c2.68,4.07,2.43,9.41-.62,13.22h0Z"/>
<path class="cls-7" d="M210.96,172.25v8.56c0,4.88-3.12,9.2-7.76,10.71l-8.13,2.65c-4.64,1.51-9.72-.13-12.59-4.08l-5.04-6.92c-2.87-3.94-2.88-9.28,0-13.23l5.02-6.93c2.86-3.95,7.94-5.6,12.58-4.1l8.14,2.63c4.64,1.5,7.78,5.82,7.79,10.69v.02Z"/>
<path class="cls-7" d="M204.48,169.3l1.13,2.01c2.39,4.25,1.78,9.56-1.53,13.14l-1.56,1.7c-3.3,3.59-8.54,4.64-12.97,2.61l-2.1-.96c-4.43-2.03-7.05-6.69-6.49-11.53l.27-2.29c.56-4.84,4.18-8.77,8.96-9.73l2.27-.46c4.78-.96,9.64,1.26,12.03,5.51h-.01Z"/>
<g>
<path class="cls-4" d="M195.39,84.89c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M282.6,148.26c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M249.28,250.89c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M141.35,250.93c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
<path class="cls-4" d="M107.97,148.29c0,.94-.76,1.7-1.7,1.7s-1.7-.76-1.7-1.7.76-1.7,1.7-1.7,1.7.76,1.7,1.7Z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,193 @@
<svg viewBox="0 0 575 250" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="sG3" x1="0" y1="0" x2=".5" y2="1">
<stop offset="0%" stop-color="#E6ECF2"/>
<stop offset="50%" stop-color="#BCC8D4"/>
<stop offset="100%" stop-color="#8090A4"/>
</linearGradient>
<linearGradient id="aG3" x1=".3" y1="0" x2=".8" y2="1">
<stop offset="0%" stop-color="#B87000"/>
<stop offset="50%" stop-color="#E0B040"/>
<stop offset="100%" stop-color="#F8D860"/>
</linearGradient>
<linearGradient id="aN3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#F5C44A"/>
<stop offset="100%" stop-color="#DC9018"/>
</linearGradient>
<linearGradient id="sN3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#A8B4C8"/>
<stop offset="100%" stop-color="#5A6A7C"/>
</linearGradient>
<clipPath id="sC3">
<path d="M300,75 C405,195 195,345 300,465 L162,408 L105,270 L162,132 Z"/>
</clipPath>
<clipPath id="aC3">
<path d="M300,75 C405,195 195,345 300,465 L438,408 L495,270 L438,132 Z"/>
</clipPath>
<filter id="nGl3" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="b"/>
<feFlood flood-color="#E8A838" flood-opacity=".1"/>
<feComposite in2="b" operator="in" result="g"/>
<feComposite in="SourceGraphic" in2="g" operator="over"/>
</filter>
<filter id="nSh3"><feDropShadow dx="0" dy="2" stdDeviation="3.5" flood-color="#DC9018" flood-opacity=".28"/></filter>
<filter id="sGl3"><feGaussianBlur stdDeviation="1.5" result="b"/><feComposite in="SourceGraphic" in2="b" operator="over"/></filter>
<filter id="cGl3"><feGaussianBlur stdDeviation="3" result="b"/><feComposite in="SourceGraphic" in2="b" operator="over"/></filter>
</defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&amp;display=swap');
@keyframes bp3{0%,100%{opacity:calc(var(--o,.82) - .20)}50%{opacity:var(--o,.82)}}
@keyframes nf3{0%,100%{transform:translate(0,0)}50%{transform:translate(0,-2px)}}
@keyframes np3{0%,100%{r:var(--r,17)}50%{r:calc(var(--r,17) + 1.8)}}
@keyframes de3{0%{stroke-dashoffset:210}100%{stroke-dashoffset:0}}
@keyframes eb3{0%,100%{opacity:.48}50%{opacity:.88}}
@keyframes ap3{0%,100%{opacity:.42}50%{opacity:.92}}
@keyframes seedAmber{0%,100%{opacity:.72;r:16}50%{opacity:.88;r:18}}
@keyframes seedSilver{0%,100%{opacity:.32}50%{opacity:.52}}
@keyframes fl3{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
@keyframes fi3{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:translateY(0)}}
@keyframes cB3{0%,100%{stroke-opacity:.1}50%{stroke-opacity:.2}}
@keyframes rP3{0%,100%{stroke-opacity:.04}50%{stroke-opacity:.068}}
@keyframes tP{0%,100%{opacity:calc(var(--to,.08)*5)}50%{opacity:calc(var(--to,.08)*5 + .25)}}
@keyframes cD{0%,100%{r:2.5;opacity:.60}50%{r:3.5;opacity:.80}}
.bp3{animation:bp3 4.5s ease-in-out infinite}
.nf3{animation:nf3 5s ease-in-out infinite}
.np3{animation:np3 3.5s ease-in-out infinite}
.de3{stroke-dasharray:210;animation:de3 1.8s ease-out forwards,eb3 4s ease-in-out 2s infinite}
.ap3{animation:ap3 3s ease-in-out infinite}
.sAm{animation:seedAmber 4s ease-in-out infinite}
.sSi{animation:seedSilver 4s ease-in-out infinite}
.fl3{animation:fl3 7s ease-in-out infinite}
.fi3{animation:fi3 1s ease-out .5s both}
.cB3{animation:cB3 5s ease-in-out infinite}
.rP3{animation:rP3 7s ease-in-out infinite}
.tP{animation:tP 5.5s ease-in-out infinite}
.cD{animation:cD 4s ease-in-out infinite}
</style>
<!-- ═══ IMAGE GROUP ═══ -->
<g id="image-group" transform="translate(-30, -22) scale(0.50)">
<!-- ═══ PAKUA FRAME ═══ -->
<path d="M300,52 L454,117 L518,270 L454,423 L300,488 L146,423 L82,270 L146,117 Z"
fill="none" stroke="#E8A838" stroke-width="1.5" stroke-opacity=".35"/>
<path d="M300,62 L447,124 L508,270 L447,416 L300,478 L153,416 L92,270 L153,124 Z"
fill="none" stroke="#C0CCD8" stroke-width="1.2" stroke-opacity=".30"/>
<!-- Trigram marks -->
<g class="tP" style="--to:.1">
<line x1="278" y1="50" x2="322" y2="50" stroke="#C0CCD8" stroke-width="2.2" stroke-linecap="round"/>
<line x1="281" y1="57" x2="319" y2="57" stroke="#C0CCD8" stroke-width="1.8" stroke-linecap="round"/>
<line x1="284" y1="63" x2="316" y2="63" stroke="#C0CCD8" stroke-width="1.5" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.08;animation-delay:2.75s">
<line x1="280" y1="488" x2="296" y2="488" stroke="#E8A838" stroke-width="2.2" stroke-linecap="round"/>
<line x1="304" y1="488" x2="320" y2="488" stroke="#E8A838" stroke-width="2.2" stroke-linecap="round"/>
<line x1="282" y1="494" x2="295" y2="494" stroke="#E8A838" stroke-width="1.8" stroke-linecap="round"/>
<line x1="305" y1="494" x2="318" y2="494" stroke="#E8A838" stroke-width="1.8" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.07;animation-delay:.7s">
<line x1="518" y1="252" x2="518" y2="266" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
<line x1="518" y1="274" x2="518" y2="278" stroke="#E8A838" stroke-width="1.5" stroke-linecap="round"/>
<line x1="518" y1="286" x2="518" y2="300" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.07;animation-delay:2s">
<line x1="82" y1="252" x2="82" y2="260" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="82" y1="268" x2="82" y2="282" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="82" y1="290" x2="82" y2="298" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:1.1s">
<line x1="462" y1="110" x2="472" y2="118" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
<line x1="458" y1="103" x2="468" y2="111" stroke="#E8A838" stroke-width="1.6" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:3.8s">
<line x1="128" y1="118" x2="138" y2="110" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="132" y1="111" x2="142" y2="103" stroke="#C0CCD8" stroke-width="1.6" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:4.4s">
<line x1="462" y1="430" x2="472" y2="422" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
<line x1="458" y1="437" x2="468" y2="429" stroke="#E8A838" stroke-width="1.6" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:5s">
<line x1="128" y1="422" x2="138" y2="430" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="132" y1="429" x2="142" y2="437" stroke="#C0CCD8" stroke-width="1.6" stroke-linecap="round"/>
</g>
<!-- Corner dots -->
<circle class="cD" cx="300" cy="52" r="2.5" fill="#C0CCD8" style="animation-delay:0s"/>
<circle class="cD" cx="454" cy="117" r="2.5" fill="#E8A838" style="animation-delay:.55s"/>
<circle class="cD" cx="518" cy="270" r="2.5" fill="#E8A838" style="animation-delay:1.1s"/>
<circle class="cD" cx="454" cy="423" r="2.5" fill="#E8A838" style="animation-delay:1.65s"/>
<circle class="cD" cx="300" cy="488" r="2.5" fill="#E8A838" style="animation-delay:2.2s"/>
<circle class="cD" cx="146" cy="423" r="2.5" fill="#C0CCD8" style="animation-delay:2.75s"/>
<circle class="cD" cx="82" cy="270" r="2.5" fill="#C0CCD8" style="animation-delay:3.3s"/>
<circle class="cD" cx="146" cy="117" r="2.5" fill="#C0CCD8" style="animation-delay:3.85s"/>
<!-- MAIN SYMBOL -->
<g class="fl3">
<path d="M300,75 L438,132 L495,270 L438,408 L300,465 L162,408 L105,270 L162,132 Z"
fill="none" stroke="rgba(255,255,255,.055)" stroke-width="1.5"/>
<!-- SILVER REGION -->
<g clip-path="url(#sC3)">
<path d="M300,75 L162,132 L105,270 L162,408 L300,465 C195,345 405,195 300,75Z" fill="url(#sG3)" opacity=".93"/>
<rect class="bp3" x="165" y="160" width="52" height="40" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".88" style="--o:.88;animation-delay:0.9s"/>
<rect class="bp3" x="235" y="223" width="48" height="38" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".82" style="--o:.82;animation-delay:0.7s"/>
<rect class="bp3" x="163" y="220" width="55" height="46" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".74" style="--o:.74;animation-delay:0.5s"/>
<rect class="bp3" x="168" y="345" width="25" height="18" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".70" style="--o:.70;animation-delay:0.15s"/>
<rect class="bp3" x="220" y="290" width="35" height="38" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".74" style="--o:.74;animation-delay:0.3s"/>
<rect class="bp3" x="160" y="285" width="40" height="32" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".64" style="--o:.64;animation-delay:0s"/>
<rect class="bp3" x="220" y="350" width="28" height="24" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".64" style="--o:.64;animation-delay:.8s"/>
<circle class="sAm" cx="265" cy="170" r="16" fill="url(#aN3)" opacity=".72" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/>
</g>
<!-- AMBER REGION -->
<g clip-path="url(#aC3)">
<path d="M300,75 L438,132 L495,270 L438,408 L300,465 C195,345 405,195 300,75Z" fill="url(#aG3)" opacity=".88"/>
<g filter="url(#nGl3)">
<line class="de3" x1="386" y1="162" x2="355" y2="214" stroke="#8090A4" stroke-width="4.5" stroke-linecap="round" opacity=".45" style="animation-delay:.15s"/>
<line class="de3" x1="405" y1="162" x2="440" y2="214" stroke="#8090A4" stroke-width="4.5" stroke-linecap="round" opacity=".45" style="animation-delay:.35s"/>
<line class="de3" x1="364" y1="247" x2="398" y2="294" stroke="#8090A4" stroke-width="4" stroke-linecap="round" opacity=".4" style="animation-delay:.6s"/>
<line class="de3" x1="431" y1="247" x2="398" y2="294" stroke="#8090A4" stroke-width="4" stroke-linecap="round" opacity=".4" style="animation-delay:.8s"/>
<line class="de3" x1="394" y1="331" x2="385" y2="368" stroke="#8090A4" stroke-width="3.5" stroke-linecap="round" opacity=".35" style="animation-delay:1.05s"/>
<polygon class="ap3" points="349,210 357,222 363,212" fill="#8090A4" opacity=".45" style="animation-delay:.15s"/>
<polygon class="ap3" points="434,210 442,222 448,210" fill="#8090A4" opacity=".45" style="animation-delay:.35s"/>
<polygon class="ap3" points="392,290 400,302 406,290" fill="#8090A4" opacity=".4" style="animation-delay:.6s"/>
<polygon class="ap3" points="379,365 387,377 393,365" fill="#8090A4" opacity=".35" style="animation-delay:1.05s"/>
</g>
<g class="nf3" style="animation-delay:0s">
<circle class="np3" cx="395" cy="148" r="17" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".75" style="--r:17"/>
</g>
<g class="nf3" style="animation-delay:.3s">
<circle class="np3" cx="355" cy="236" r="14" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".7" style="--r:14"/>
</g>
<g class="nf3" style="animation-delay:.5s">
<circle class="np3" cx="440" cy="236" r="14" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".7" style="--r:14"/>
</g>
<g class="nf3" style="animation-delay:.8s">
<circle class="np3" cx="398" cy="316" r="15" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".72" style="--r:15"/>
</g>
<g class="nf3" style="animation-delay:1.1s">
<circle class="np3" cx="385" cy="390" r="12" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".65" style="--r:12"/>
</g>
<rect class="sAm" x="320" y="330" width="26" height="26" rx="3" fill="#5A6A7C" opacity=".85" stroke="rgba(255,255,255,.35)" stroke-width="1.5"/>
</g>
<!-- S-CURVE -->
<path class="cB3" d="M300,75 C405,195 195,345 300,465"
fill="none" stroke="rgba(255,255,255,.1)" stroke-width="1.8" filter="url(#cGl3)"/>
</g>
</g>
<!-- ═══ TEXT GROUP ═══ -->
<g id="text-group" class="fi3">
<text x="400" y="131" text-anchor="middle" font-family="'IBM Plex Mono',monospace" font-size="72" font-weight="600" letter-spacing=".03em">
<tspan fill="#C0CCD8">Onto</tspan><tspan fill="#E8A838">ref</tspan>
</text>
<text x="398" y="156" text-anchor="middle" font-family="'IBM Plex Sans',sans-serif" font-size="16" font-weight="500" fill="#A8B4C8" letter-spacing=".03em" opacity=".9">
Structure that remembers why
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,193 @@
<svg viewBox="0 0 575 250" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="sG3" x1="0" y1="0" x2=".5" y2="1">
<stop offset="0%" stop-color="#E6ECF2"/>
<stop offset="50%" stop-color="#BCC8D4"/>
<stop offset="100%" stop-color="#8090A4"/>
</linearGradient>
<linearGradient id="aG3" x1=".3" y1="0" x2=".8" y2="1">
<stop offset="0%" stop-color="#B87000"/>
<stop offset="50%" stop-color="#E0B040"/>
<stop offset="100%" stop-color="#F8D860"/>
</linearGradient>
<linearGradient id="aN3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#F5C44A"/>
<stop offset="100%" stop-color="#DC9018"/>
</linearGradient>
<linearGradient id="sN3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#A8B4C8"/>
<stop offset="100%" stop-color="#5A6A7C"/>
</linearGradient>
<clipPath id="sC3">
<path d="M300,75 C405,195 195,345 300,465 L162,408 L105,270 L162,132 Z"/>
</clipPath>
<clipPath id="aC3">
<path d="M300,75 C405,195 195,345 300,465 L438,408 L495,270 L438,132 Z"/>
</clipPath>
<filter id="nGl3" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="b"/>
<feFlood flood-color="#E8A838" flood-opacity=".1"/>
<feComposite in2="b" operator="in" result="g"/>
<feComposite in="SourceGraphic" in2="g" operator="over"/>
</filter>
<filter id="nSh3"><feDropShadow dx="0" dy="2" stdDeviation="3.5" flood-color="#DC9018" flood-opacity=".28"/></filter>
<filter id="sGl3"><feGaussianBlur stdDeviation="1.5" result="b"/><feComposite in="SourceGraphic" in2="b" operator="over"/></filter>
<filter id="cGl3"><feGaussianBlur stdDeviation="3" result="b"/><feComposite in="SourceGraphic" in2="b" operator="over"/></filter>
</defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&amp;display=swap');
@keyframes bp3{0%,100%{opacity:calc(var(--o,.82) - .20)}50%{opacity:var(--o,.82)}}
@keyframes nf3{0%,100%{transform:translate(0,0)}50%{transform:translate(0,-2px)}}
@keyframes np3{0%,100%{r:var(--r,17)}50%{r:calc(var(--r,17) + 1.8)}}
@keyframes de3{0%{stroke-dashoffset:210}100%{stroke-dashoffset:0}}
@keyframes eb3{0%,100%{opacity:.48}50%{opacity:.88}}
@keyframes ap3{0%,100%{opacity:.42}50%{opacity:.92}}
@keyframes seedAmber{0%,100%{opacity:.72;r:16}50%{opacity:.88;r:18}}
@keyframes seedSilver{0%,100%{opacity:.32}50%{opacity:.52}}
@keyframes fl3{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
@keyframes fi3{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:translateY(0)}}
@keyframes cB3{0%,100%{stroke-opacity:.1}50%{stroke-opacity:.2}}
@keyframes rP3{0%,100%{stroke-opacity:.04}50%{stroke-opacity:.068}}
@keyframes tP{0%,100%{opacity:calc(var(--to,.08)*5)}50%{opacity:calc(var(--to,.08)*5 + .25)}}
@keyframes cD{0%,100%{r:2.5;opacity:.60}50%{r:3.5;opacity:.80}}
.bp3{animation:bp3 4.5s ease-in-out infinite}
.nf3{animation:nf3 5s ease-in-out infinite}
.np3{animation:np3 3.5s ease-in-out infinite}
.de3{stroke-dasharray:210;animation:de3 1.8s ease-out forwards,eb3 4s ease-in-out 2s infinite}
.ap3{animation:ap3 3s ease-in-out infinite}
.sAm{animation:seedAmber 4s ease-in-out infinite}
.sSi{animation:seedSilver 4s ease-in-out infinite}
.fl3{animation:fl3 7s ease-in-out infinite}
.fi3{animation:fi3 1s ease-out .5s both}
.cB3{animation:cB3 5s ease-in-out infinite}
.rP3{animation:rP3 7s ease-in-out infinite}
.tP{animation:tP 5.5s ease-in-out infinite}
.cD{animation:cD 4s ease-in-out infinite}
</style>
<!-- ═══ IMAGE GROUP ═══ -->
<g id="image-group" transform="translate(-30, -22) scale(0.50)">
<!-- ═══ PAKUA FRAME ═══ -->
<path d="M300,52 L454,117 L518,270 L454,423 L300,488 L146,423 L82,270 L146,117 Z"
fill="none" stroke="#E8A838" stroke-width="1.5" stroke-opacity=".35"/>
<path d="M300,62 L447,124 L508,270 L447,416 L300,478 L153,416 L92,270 L153,124 Z"
fill="none" stroke="#C0CCD8" stroke-width="1.2" stroke-opacity=".30"/>
<!-- Trigram marks -->
<g class="tP" style="--to:.1">
<line x1="278" y1="50" x2="322" y2="50" stroke="#C0CCD8" stroke-width="2.2" stroke-linecap="round"/>
<line x1="281" y1="57" x2="319" y2="57" stroke="#C0CCD8" stroke-width="1.8" stroke-linecap="round"/>
<line x1="284" y1="63" x2="316" y2="63" stroke="#C0CCD8" stroke-width="1.5" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.08;animation-delay:2.75s">
<line x1="280" y1="488" x2="296" y2="488" stroke="#E8A838" stroke-width="2.2" stroke-linecap="round"/>
<line x1="304" y1="488" x2="320" y2="488" stroke="#E8A838" stroke-width="2.2" stroke-linecap="round"/>
<line x1="282" y1="494" x2="295" y2="494" stroke="#E8A838" stroke-width="1.8" stroke-linecap="round"/>
<line x1="305" y1="494" x2="318" y2="494" stroke="#E8A838" stroke-width="1.8" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.07;animation-delay:.7s">
<line x1="518" y1="252" x2="518" y2="266" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
<line x1="518" y1="274" x2="518" y2="278" stroke="#E8A838" stroke-width="1.5" stroke-linecap="round"/>
<line x1="518" y1="286" x2="518" y2="300" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.07;animation-delay:2s">
<line x1="82" y1="252" x2="82" y2="260" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="82" y1="268" x2="82" y2="282" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="82" y1="290" x2="82" y2="298" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:1.1s">
<line x1="462" y1="110" x2="472" y2="118" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
<line x1="458" y1="103" x2="468" y2="111" stroke="#E8A838" stroke-width="1.6" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:3.8s">
<line x1="128" y1="118" x2="138" y2="110" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="132" y1="111" x2="142" y2="103" stroke="#C0CCD8" stroke-width="1.6" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:4.4s">
<line x1="462" y1="430" x2="472" y2="422" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
<line x1="458" y1="437" x2="468" y2="429" stroke="#E8A838" stroke-width="1.6" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:5s">
<line x1="128" y1="422" x2="138" y2="430" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="132" y1="429" x2="142" y2="437" stroke="#C0CCD8" stroke-width="1.6" stroke-linecap="round"/>
</g>
<!-- Corner dots -->
<circle class="cD" cx="300" cy="52" r="2.5" fill="#C0CCD8" style="animation-delay:0s"/>
<circle class="cD" cx="454" cy="117" r="2.5" fill="#E8A838" style="animation-delay:.55s"/>
<circle class="cD" cx="518" cy="270" r="2.5" fill="#E8A838" style="animation-delay:1.1s"/>
<circle class="cD" cx="454" cy="423" r="2.5" fill="#E8A838" style="animation-delay:1.65s"/>
<circle class="cD" cx="300" cy="488" r="2.5" fill="#E8A838" style="animation-delay:2.2s"/>
<circle class="cD" cx="146" cy="423" r="2.5" fill="#C0CCD8" style="animation-delay:2.75s"/>
<circle class="cD" cx="82" cy="270" r="2.5" fill="#C0CCD8" style="animation-delay:3.3s"/>
<circle class="cD" cx="146" cy="117" r="2.5" fill="#C0CCD8" style="animation-delay:3.85s"/>
<!-- MAIN SYMBOL -->
<g class="fl3">
<path d="M300,75 L438,132 L495,270 L438,408 L300,465 L162,408 L105,270 L162,132 Z"
fill="none" stroke="rgba(255,255,255,.055)" stroke-width="1.5"/>
<!-- SILVER REGION -->
<g clip-path="url(#sC3)">
<path d="M300,75 L162,132 L105,270 L162,408 L300,465 C195,345 405,195 300,75Z" fill="url(#sG3)" opacity=".93"/>
<rect class="bp3" x="165" y="160" width="52" height="40" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".88" style="--o:.88;animation-delay:0.9s"/>
<rect class="bp3" x="235" y="223" width="48" height="38" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".82" style="--o:.82;animation-delay:0.7s"/>
<rect class="bp3" x="163" y="220" width="55" height="46" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".74" style="--o:.74;animation-delay:0.5s"/>
<rect class="bp3" x="168" y="345" width="25" height="18" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".70" style="--o:.70;animation-delay:0.15s"/>
<rect class="bp3" x="220" y="290" width="35" height="38" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".74" style="--o:.74;animation-delay:0.3s"/>
<rect class="bp3" x="160" y="285" width="40" height="32" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".64" style="--o:.64;animation-delay:0s"/>
<rect class="bp3" x="220" y="350" width="28" height="24" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".64" style="--o:.64;animation-delay:.8s"/>
<circle class="sAm" cx="265" cy="170" r="16" fill="url(#aN3)" opacity=".72" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/>
</g>
<!-- AMBER REGION -->
<g clip-path="url(#aC3)">
<path d="M300,75 L438,132 L495,270 L438,408 L300,465 C195,345 405,195 300,75Z" fill="url(#aG3)" opacity=".88"/>
<g filter="url(#nGl3)">
<line class="de3" x1="386" y1="162" x2="355" y2="214" stroke="#8090A4" stroke-width="4.5" stroke-linecap="round" opacity=".45" style="animation-delay:.15s"/>
<line class="de3" x1="405" y1="162" x2="440" y2="214" stroke="#8090A4" stroke-width="4.5" stroke-linecap="round" opacity=".45" style="animation-delay:.35s"/>
<line class="de3" x1="364" y1="247" x2="398" y2="294" stroke="#8090A4" stroke-width="4" stroke-linecap="round" opacity=".4" style="animation-delay:.6s"/>
<line class="de3" x1="431" y1="247" x2="398" y2="294" stroke="#8090A4" stroke-width="4" stroke-linecap="round" opacity=".4" style="animation-delay:.8s"/>
<line class="de3" x1="394" y1="331" x2="385" y2="368" stroke="#8090A4" stroke-width="3.5" stroke-linecap="round" opacity=".35" style="animation-delay:1.05s"/>
<polygon class="ap3" points="349,210 357,222 363,212" fill="#8090A4" opacity=".45" style="animation-delay:.15s"/>
<polygon class="ap3" points="434,210 442,222 448,210" fill="#8090A4" opacity=".45" style="animation-delay:.35s"/>
<polygon class="ap3" points="392,290 400,302 406,290" fill="#8090A4" opacity=".4" style="animation-delay:.6s"/>
<polygon class="ap3" points="379,365 387,377 393,365" fill="#8090A4" opacity=".35" style="animation-delay:1.05s"/>
</g>
<g class="nf3" style="animation-delay:0s">
<circle class="np3" cx="395" cy="148" r="17" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".75" style="--r:17"/>
</g>
<g class="nf3" style="animation-delay:.3s">
<circle class="np3" cx="355" cy="236" r="14" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".7" style="--r:14"/>
</g>
<g class="nf3" style="animation-delay:.5s">
<circle class="np3" cx="440" cy="236" r="14" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".7" style="--r:14"/>
</g>
<g class="nf3" style="animation-delay:.8s">
<circle class="np3" cx="398" cy="316" r="15" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".72" style="--r:15"/>
</g>
<g class="nf3" style="animation-delay:1.1s">
<circle class="np3" cx="385" cy="390" r="12" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".65" style="--r:12"/>
</g>
<rect class="sAm" x="320" y="330" width="26" height="26" rx="3" fill="#5A6A7C" opacity=".85" stroke="rgba(255,255,255,.35)" stroke-width="1.5"/>
</g>
<!-- S-CURVE -->
<path class="cB3" d="M300,75 C405,195 195,345 300,465"
fill="none" stroke="rgba(255,255,255,.1)" stroke-width="1.8" filter="url(#cGl3)"/>
</g>
</g>
<!-- ═══ TEXT GROUP ═══ -->
<g id="text-group" class="fi3">
<text x="400" y="131" text-anchor="middle" font-family="'IBM Plex Mono',monospace" font-size="72" font-weight="600" letter-spacing=".03em">
<tspan fill="#C0CCD8">Onto</tspan><tspan fill="#E8A838">ref</tspan>
</text>
<text x="398" y="156" text-anchor="middle" font-family="'IBM Plex Sans',sans-serif" font-size="16" font-weight="500" fill="#A8B4C8" letter-spacing=".03em" opacity=".9">
Structure that remembers why
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,19 @@
<svg viewBox="0 0 400 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&amp;display=swap');
@keyframes fi3{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:translateY(0)}}
.fi3{animation:fi3 1s ease-out .5s both}
</style>
</defs>
<!-- Wordmark -->
<g class="fi3">
<text x="200" y="55" text-anchor="middle" font-family="'IBM Plex Mono',monospace" font-size="92" font-weight="600" letter-spacing=".03em">
<tspan fill="#C0CCD8">Onto</tspan><tspan fill="#E8A838">ref</tspan>
</text>
<text x="200" y="105" text-anchor="middle" font-family="'IBM Plex Sans',sans-serif" font-size="22" font-weight="500" fill="#A8B4C8" letter-spacing=".03em" opacity=".9">
Structure that remembers why.
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 878 B

View file

@ -0,0 +1,193 @@
<svg viewBox="0 0 380 340" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="sG3" x1="0" y1="0" x2=".5" y2="1">
<stop offset="0%" stop-color="#E6ECF2"/>
<stop offset="50%" stop-color="#BCC8D4"/>
<stop offset="100%" stop-color="#8090A4"/>
</linearGradient>
<linearGradient id="aG3" x1=".3" y1="0" x2=".8" y2="1">
<stop offset="0%" stop-color="#B87000"/>
<stop offset="50%" stop-color="#E0B040"/>
<stop offset="100%" stop-color="#F8D860"/>
</linearGradient>
<linearGradient id="aN3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#F5C44A"/>
<stop offset="100%" stop-color="#DC9018"/>
</linearGradient>
<linearGradient id="sN3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#A8B4C8"/>
<stop offset="100%" stop-color="#5A6A7C"/>
</linearGradient>
<clipPath id="sC3">
<path d="M300,75 C405,195 195,345 300,465 L162,408 L105,270 L162,132 Z"/>
</clipPath>
<clipPath id="aC3">
<path d="M300,75 C405,195 195,345 300,465 L438,408 L495,270 L438,132 Z"/>
</clipPath>
<filter id="nGl3" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="b"/>
<feFlood flood-color="#E8A838" flood-opacity=".1"/>
<feComposite in2="b" operator="in" result="g"/>
<feComposite in="SourceGraphic" in2="g" operator="over"/>
</filter>
<filter id="nSh3"><feDropShadow dx="0" dy="2" stdDeviation="3.5" flood-color="#DC9018" flood-opacity=".28"/></filter>
<filter id="sGl3"><feGaussianBlur stdDeviation="1.5" result="b"/><feComposite in="SourceGraphic" in2="b" operator="over"/></filter>
<filter id="cGl3"><feGaussianBlur stdDeviation="3" result="b"/><feComposite in="SourceGraphic" in2="b" operator="over"/></filter>
</defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&amp;display=swap');
@keyframes bp3{0%,100%{opacity:calc(var(--o,.82) - .20)}50%{opacity:var(--o,.82)}}
@keyframes nf3{0%,100%{transform:translate(0,0)}50%{transform:translate(0,-2px)}}
@keyframes np3{0%,100%{r:var(--r,17)}50%{r:calc(var(--r,17) + 1.8)}}
@keyframes de3{0%{stroke-dashoffset:210}100%{stroke-dashoffset:0}}
@keyframes eb3{0%,100%{opacity:.48}50%{opacity:.88}}
@keyframes ap3{0%,100%{opacity:.42}50%{opacity:.92}}
@keyframes seedAmber{0%,100%{opacity:.72;r:16}50%{opacity:.88;r:18}}
@keyframes seedSilver{0%,100%{opacity:.32}50%{opacity:.52}}
@keyframes fl3{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
@keyframes fi3{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:translateY(0)}}
@keyframes cB3{0%,100%{stroke-opacity:.1}50%{stroke-opacity:.2}}
@keyframes rP3{0%,100%{stroke-opacity:.04}50%{stroke-opacity:.068}}
@keyframes tP{0%,100%{opacity:calc(var(--to,.08)*5)}50%{opacity:calc(var(--to,.08)*5 + .25)}}
@keyframes cD{0%,100%{r:2.5;opacity:.60}50%{r:3.5;opacity:.80}}
.bp3{animation:bp3 4.5s ease-in-out infinite}
.nf3{animation:nf3 5s ease-in-out infinite}
.np3{animation:np3 3.5s ease-in-out infinite}
.de3{stroke-dasharray:210;animation:de3 1.8s ease-out forwards,eb3 4s ease-in-out 2s infinite}
.ap3{animation:ap3 3s ease-in-out infinite}
.sAm{animation:seedAmber 4s ease-in-out infinite}
.sSi{animation:seedSilver 4s ease-in-out infinite}
.fl3{animation:fl3 7s ease-in-out infinite}
.fi3{animation:fi3 1s ease-out .5s both}
.cB3{animation:cB3 5s ease-in-out infinite}
.rP3{animation:rP3 7s ease-in-out infinite}
.tP{animation:tP 5.5s ease-in-out infinite}
.cD{animation:cD 4s ease-in-out infinite}
</style>
<!-- ═══ IMAGE GROUP ═══ -->
<g id="image-group" transform="translate(40, -20) scale(0.50)">
<!-- ═══ PAKUA FRAME ═══ -->
<path d="M300,52 L454,117 L518,270 L454,423 L300,488 L146,423 L82,270 L146,117 Z"
fill="none" stroke="#E8A838" stroke-width="1.5" stroke-opacity=".35"/>
<path d="M300,62 L447,124 L508,270 L447,416 L300,478 L153,416 L92,270 L153,124 Z"
fill="none" stroke="#C0CCD8" stroke-width="1.2" stroke-opacity=".30"/>
<!-- Trigram marks -->
<g class="tP" style="--to:.1">
<line x1="278" y1="50" x2="322" y2="50" stroke="#C0CCD8" stroke-width="2.2" stroke-linecap="round"/>
<line x1="281" y1="57" x2="319" y2="57" stroke="#C0CCD8" stroke-width="1.8" stroke-linecap="round"/>
<line x1="284" y1="63" x2="316" y2="63" stroke="#C0CCD8" stroke-width="1.5" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.08;animation-delay:2.75s">
<line x1="280" y1="488" x2="296" y2="488" stroke="#E8A838" stroke-width="2.2" stroke-linecap="round"/>
<line x1="304" y1="488" x2="320" y2="488" stroke="#E8A838" stroke-width="2.2" stroke-linecap="round"/>
<line x1="282" y1="494" x2="295" y2="494" stroke="#E8A838" stroke-width="1.8" stroke-linecap="round"/>
<line x1="305" y1="494" x2="318" y2="494" stroke="#E8A838" stroke-width="1.8" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.07;animation-delay:.7s">
<line x1="518" y1="252" x2="518" y2="266" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
<line x1="518" y1="274" x2="518" y2="278" stroke="#E8A838" stroke-width="1.5" stroke-linecap="round"/>
<line x1="518" y1="286" x2="518" y2="300" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.07;animation-delay:2s">
<line x1="82" y1="252" x2="82" y2="260" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="82" y1="268" x2="82" y2="282" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="82" y1="290" x2="82" y2="298" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:1.1s">
<line x1="462" y1="110" x2="472" y2="118" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
<line x1="458" y1="103" x2="468" y2="111" stroke="#E8A838" stroke-width="1.6" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:3.8s">
<line x1="128" y1="118" x2="138" y2="110" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="132" y1="111" x2="142" y2="103" stroke="#C0CCD8" stroke-width="1.6" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:4.4s">
<line x1="462" y1="430" x2="472" y2="422" stroke="#E8A838" stroke-width="2" stroke-linecap="round"/>
<line x1="458" y1="437" x2="468" y2="429" stroke="#E8A838" stroke-width="1.6" stroke-linecap="round"/>
</g>
<g class="tP" style="--to:.06;animation-delay:5s">
<line x1="128" y1="422" x2="138" y2="430" stroke="#C0CCD8" stroke-width="2" stroke-linecap="round"/>
<line x1="132" y1="429" x2="142" y2="437" stroke="#C0CCD8" stroke-width="1.6" stroke-linecap="round"/>
</g>
<!-- Corner dots -->
<circle class="cD" cx="300" cy="52" r="2.5" fill="#C0CCD8" style="animation-delay:0s"/>
<circle class="cD" cx="454" cy="117" r="2.5" fill="#E8A838" style="animation-delay:.55s"/>
<circle class="cD" cx="518" cy="270" r="2.5" fill="#E8A838" style="animation-delay:1.1s"/>
<circle class="cD" cx="454" cy="423" r="2.5" fill="#E8A838" style="animation-delay:1.65s"/>
<circle class="cD" cx="300" cy="488" r="2.5" fill="#E8A838" style="animation-delay:2.2s"/>
<circle class="cD" cx="146" cy="423" r="2.5" fill="#C0CCD8" style="animation-delay:2.75s"/>
<circle class="cD" cx="82" cy="270" r="2.5" fill="#C0CCD8" style="animation-delay:3.3s"/>
<circle class="cD" cx="146" cy="117" r="2.5" fill="#C0CCD8" style="animation-delay:3.85s"/>
<!-- MAIN SYMBOL -->
<g class="fl3">
<path d="M300,75 L438,132 L495,270 L438,408 L300,465 L162,408 L105,270 L162,132 Z"
fill="none" stroke="rgba(255,255,255,.055)" stroke-width="1.5"/>
<!-- SILVER REGION -->
<g clip-path="url(#sC3)">
<path d="M300,75 L162,132 L105,270 L162,408 L300,465 C195,345 405,195 300,75Z" fill="url(#sG3)" opacity=".93"/>
<rect class="bp3" x="165" y="160" width="52" height="40" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".88" style="--o:.88;animation-delay:0.9s"/>
<rect class="bp3" x="235" y="223" width="48" height="38" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".82" style="--o:.82;animation-delay:0.7s"/>
<rect class="bp3" x="163" y="220" width="55" height="46" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".74" style="--o:.74;animation-delay:0.5s"/>
<rect class="bp3" x="168" y="345" width="25" height="18" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".70" style="--o:.70;animation-delay:0.15s"/>
<rect class="bp3" x="220" y="290" width="35" height="38" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".74" style="--o:.74;animation-delay:0.3s"/>
<rect class="bp3" x="160" y="285" width="40" height="32" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".64" style="--o:.64;animation-delay:0s"/>
<rect class="bp3" x="220" y="350" width="28" height="24" rx="4" fill="url(#aG3)" stroke="rgba(255,255,255,.25)" stroke-width="1" opacity=".64" style="--o:.64;animation-delay:.8s"/>
<circle class="sAm" cx="265" cy="170" r="16" fill="url(#aN3)" opacity=".72" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/>
</g>
<!-- AMBER REGION -->
<g clip-path="url(#aC3)">
<path d="M300,75 L438,132 L495,270 L438,408 L300,465 C195,345 405,195 300,75Z" fill="url(#aG3)" opacity=".88"/>
<g filter="url(#nGl3)">
<line class="de3" x1="386" y1="162" x2="355" y2="214" stroke="#8090A4" stroke-width="4.5" stroke-linecap="round" opacity=".45" style="animation-delay:.15s"/>
<line class="de3" x1="405" y1="162" x2="440" y2="214" stroke="#8090A4" stroke-width="4.5" stroke-linecap="round" opacity=".45" style="animation-delay:.35s"/>
<line class="de3" x1="364" y1="247" x2="398" y2="294" stroke="#8090A4" stroke-width="4" stroke-linecap="round" opacity=".4" style="animation-delay:.6s"/>
<line class="de3" x1="431" y1="247" x2="398" y2="294" stroke="#8090A4" stroke-width="4" stroke-linecap="round" opacity=".4" style="animation-delay:.8s"/>
<line class="de3" x1="394" y1="331" x2="385" y2="368" stroke="#8090A4" stroke-width="3.5" stroke-linecap="round" opacity=".35" style="animation-delay:1.05s"/>
<polygon class="ap3" points="349,210 357,222 363,212" fill="#8090A4" opacity=".45" style="animation-delay:.15s"/>
<polygon class="ap3" points="434,210 442,222 448,210" fill="#8090A4" opacity=".45" style="animation-delay:.35s"/>
<polygon class="ap3" points="392,290 400,302 406,290" fill="#8090A4" opacity=".4" style="animation-delay:.6s"/>
<polygon class="ap3" points="379,365 387,377 393,365" fill="#8090A4" opacity=".35" style="animation-delay:1.05s"/>
</g>
<g class="nf3" style="animation-delay:0s">
<circle class="np3" cx="395" cy="148" r="17" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".75" style="--r:17"/>
</g>
<g class="nf3" style="animation-delay:.3s">
<circle class="np3" cx="355" cy="236" r="14" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".7" style="--r:14"/>
</g>
<g class="nf3" style="animation-delay:.5s">
<circle class="np3" cx="440" cy="236" r="14" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".7" style="--r:14"/>
</g>
<g class="nf3" style="animation-delay:.8s">
<circle class="np3" cx="398" cy="316" r="15" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".72" style="--r:15"/>
</g>
<g class="nf3" style="animation-delay:1.1s">
<circle class="np3" cx="385" cy="390" r="12" fill="url(#sN3)" stroke="rgba(255,255,255,.3)" stroke-width="1.5" opacity=".65" style="--r:12"/>
</g>
<rect class="sAm" x="320" y="330" width="26" height="26" rx="3" fill="#5A6A7C" opacity=".85" stroke="rgba(255,255,255,.35)" stroke-width="1.5"/>
</g>
<!-- S-CURVE -->
<path class="cB3" d="M300,75 C405,195 195,345 300,465"
fill="none" stroke="rgba(255,255,255,.1)" stroke-width="1.8" filter="url(#cGl3)"/>
</g>
</g>
<!-- ═══ TEXT GROUP ═══ -->
<g id="text-group" class="fi3">
<text x="190" y="290" text-anchor="middle" font-family="'IBM Plex Mono',monospace" font-size="72" font-weight="600" letter-spacing=".03em">
<tspan fill="#C0CCD8">Onto</tspan><tspan fill="#E8A838">ref</tspan>
</text>
<text x="190" y="315" text-anchor="middle" font-family="'IBM Plex Sans',sans-serif" font-size="16" font-weight="500" fill="#A8B4C8" letter-spacing=".03em" opacity=".9">
Structure that remembers why
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,273 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="578" viewBox="0 0 900 578">
<defs>
<marker id="a" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#475569"/>
</marker>
<marker id="ab" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#60a5fa"/>
</marker>
<marker id="ao" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#E8A838"/>
</marker>
<marker id="ag" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#4ade80"/>
</marker>
<marker id="ar" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#f87171"/>
</marker>
</defs>
<!-- Background -->
<rect width="900" height="578" fill="#0f172a"/>
<!-- ══════════════════════════════════════════════════════════════
SECTION 1 — PROTOCOL LAYERS
══════════════════════════════════════════════════════════════ -->
<!-- section label -->
<text x="30" y="20" font-family="'JetBrains Mono',monospace" font-size="8.5" fill="#334155" letter-spacing="2" font-weight="700">PROTOCOL LAYERS</text>
<!-- Left connector spine (dashed) -->
<line x1="31.5" y1="30" x2="31.5" y2="298" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="3 3"/>
<!-- ── L1: DECLARATIVE / NICKEL ── y=30 -->
<rect x="28" y="30" width="844" height="40" rx="3" fill="#60a5fa" fill-opacity="0.07" stroke="#60a5fa" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="30" width="3" height="40" rx="1" fill="#60a5fa"/>
<text x="40" y="46" font-family="'JetBrains Mono',monospace" font-size="8" fill="#60a5fa" font-weight="700" letter-spacing="1.5">DECLARATIVE · NICKEL</text>
<text x="40" y="60" font-family="'JetBrains Mono',monospace" font-size="7" fill="#60a5fa" fill-opacity="0.6">type-safe contracts · fails at definition time</text>
<!-- chips -->
<rect x="222" y="36" width="52" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="248" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">.ontology/</text>
<rect x="280" y="36" width="46" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="303" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">adrs/*.ncl</text>
<rect x="332" y="36" width="56" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="360" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">modes/*.ncl</text>
<rect x="394" y="36" width="56" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="422" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">schemas/*.ncl</text>
<rect x="456" y="36" width="40" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="476" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">qa.ncl</text>
<rect x="502" y="36" width="56" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="530" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">config.ncl</text>
<!-- inter-layer arrow + label -->
<line x1="31.5" y1="70" x2="31.5" y2="77" stroke="#60a5fa" stroke-opacity="0.4" stroke-width="1" marker-end="url(#ab)"/>
<text x="42" y="76" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#334155" letter-spacing="0.5">nickel export → JSON</text>
<!-- ── L2: KNOWLEDGE GRAPH ── y=80 -->
<rect x="28" y="80" width="844" height="40" rx="3" fill="#c084fc" fill-opacity="0.07" stroke="#c084fc" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="80" width="3" height="40" rx="1" fill="#c084fc"/>
<text x="40" y="96" font-family="'JetBrains Mono',monospace" font-size="8" fill="#c084fc" font-weight="700" letter-spacing="1.5">KNOWLEDGE GRAPH · .ontology/</text>
<text x="40" y="110" font-family="'JetBrains Mono',monospace" font-size="7" fill="#c084fc" fill-opacity="0.6">self-describing · actor-agnostic · machine-queryable</text>
<!-- chips -->
<rect x="222" y="86" width="42" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="243" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">axioms</text>
<rect x="270" y="86" width="46" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="293" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">tensions</text>
<rect x="322" y="86" width="52" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="348" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">practices</text>
<rect x="380" y="86" width="62" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="411" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">nodes + edges</text>
<rect x="448" y="86" width="50" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="473" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">invariants</text>
<rect x="504" y="86" width="34" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="521" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">gates</text>
<!-- inter-layer arrow -->
<line x1="31.5" y1="120" x2="31.5" y2="127" stroke="#f97316" stroke-opacity="0.4" stroke-width="1" marker-end="url(#ao)"/>
<text x="42" y="126" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#334155" letter-spacing="0.5">reads KG via nickel export</text>
<!-- ── L3: OPERATIONAL / NUSHELL ── y=130 -->
<rect x="28" y="130" width="844" height="40" rx="3" fill="#f97316" fill-opacity="0.07" stroke="#f97316" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="130" width="3" height="40" rx="1" fill="#f97316"/>
<text x="40" y="146" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f97316" font-weight="700" letter-spacing="1.5">OPERATIONAL · NUSHELL</text>
<text x="40" y="160" font-family="'JetBrains Mono',monospace" font-size="7" fill="#f97316" fill-opacity="0.6">16 modules · DAG-validated modes · typed pipelines</text>
<!-- chips -->
<rect x="222" y="136" width="40" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="242" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">adr.nu</text>
<rect x="268" y="136" width="52" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="294" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">backlog.nu</text>
<rect x="326" y="136" width="54" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="353" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">describe.nu</text>
<rect x="386" y="136" width="44" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="408" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">sync.nu</text>
<rect x="436" y="136" width="46" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="459" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">coder.nu</text>
<rect x="488" y="136" width="54" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="515" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">+11 modules</text>
<!-- inter-layer arrow -->
<line x1="31.5" y1="170" x2="31.5" y2="177" stroke="#4ade80" stroke-opacity="0.4" stroke-width="1" marker-end="url(#ag)"/>
<text x="42" y="176" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#334155" letter-spacing="0.5">dispatches command + actor</text>
<!-- ── L4: ENTRY POINT / BASH→NU ── y=180 -->
<rect x="28" y="180" width="844" height="40" rx="3" fill="#4ade80" fill-opacity="0.07" stroke="#4ade80" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="180" width="3" height="40" rx="1" fill="#4ade80"/>
<text x="40" y="196" font-family="'JetBrains Mono',monospace" font-size="8" fill="#4ade80" font-weight="700" letter-spacing="1.5">ENTRY POINT · BASH → NU</text>
<text x="40" y="210" font-family="'JetBrains Mono',monospace" font-size="7" fill="#4ade80" fill-opacity="0.6">actor detect · advisory lock · NICKEL_IMPORT_PATH</text>
<!-- chips -->
<rect x="222" y="186" width="52" height="16" rx="3" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="248" y="197" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#4ade80">./ontoref</text>
<rect x="280" y="186" width="62" height="16" rx="3" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="311" y="197" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#4ade80">actor detect</text>
<rect x="348" y="186" width="64" height="16" rx="3" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="380" y="197" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#4ade80">advisory lock</text>
<rect x="418" y="186" width="62" height="16" rx="3" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="449" y="197" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#4ade80">→ ontoref.nu</text>
<!-- inter-layer arrow -->
<line x1="31.5" y1="220" x2="31.5" y2="227" stroke="#22d3ee" stroke-opacity="0.4" stroke-width="1" marker-end="url(#a)"/>
<text x="42" y="226" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#334155" letter-spacing="0.5">optional · caches · serves · never required</text>
<!-- ── L5: RUNTIME / RUST + axum ── y=230 -->
<rect x="28" y="230" width="844" height="40" rx="3" fill="#22d3ee" fill-opacity="0.07" stroke="#22d3ee" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="230" width="3" height="40" rx="1" fill="#22d3ee"/>
<text x="40" y="246" font-family="'JetBrains Mono',monospace" font-size="8" fill="#22d3ee" font-weight="700" letter-spacing="1.5">RUNTIME · RUST + axum (optional daemon)</text>
<text x="40" y="260" font-family="'JetBrains Mono',monospace" font-size="7" fill="#22d3ee" fill-opacity="0.6">10 UI pages · 19 MCP tools · actor registry · search · SurrealDB?</text>
<!-- chips -->
<rect x="222" y="236" width="76" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="260" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">ontoref-daemon</text>
<rect x="304" y="236" width="56" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="332" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">axum HTTP</text>
<rect x="366" y="236" width="46" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="389" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">MCP ×19</text>
<rect x="418" y="236" width="46" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="441" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">DashMap</text>
<rect x="470" y="236" width="38" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="489" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">search</text>
<rect x="514" y="236" width="56" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="542" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">SurrealDB?</text>
<!-- inter-layer arrow -->
<line x1="31.5" y1="270" x2="31.5" y2="277" stroke="#facc15" stroke-opacity="0.4" stroke-width="1" marker-end="url(#a)"/>
<text x="42" y="276" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#334155" letter-spacing="0.5">configures · ONTOREF_PROJECT_ROOT</text>
<!-- ── L6: ADOPTION ── y=280 -->
<rect x="28" y="280" width="844" height="40" rx="3" fill="#facc15" fill-opacity="0.07" stroke="#facc15" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="280" width="3" height="40" rx="1" fill="#facc15"/>
<text x="40" y="296" font-family="'JetBrains Mono',monospace" font-size="8" fill="#facc15" font-weight="700" letter-spacing="1.5">ADOPTION · PER-PROJECT</text>
<text x="40" y="310" font-family="'JetBrains Mono',monospace" font-size="7" fill="#facc15" fill-opacity="0.6">templates/ · one ontoref checkout · zero lock-in</text>
<!-- chips -->
<rect x="222" y="286" width="56" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="250" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">templates/</text>
<rect x="284" y="286" width="74" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="321" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">adopt_ontoref</text>
<rect x="364" y="286" width="108" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="418" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">ONTOREF_PROJECT_ROOT</text>
<rect x="478" y="286" width="62" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="509" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">zero lock-in</text>
<rect x="546" y="286" width="58" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="575" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">plain NCL</text>
<!-- ══════════════════════════════════════════════════════════════
DIVIDER
══════════════════════════════════════════════════════════════ -->
<line x1="28" y1="334" x2="872" y2="334" stroke="#1e293b" stroke-width="1"/>
<!-- ══════════════════════════════════════════════════════════════
SECTION 2 — REQUEST FLOWS
══════════════════════════════════════════════════════════════ -->
<text x="30" y="348" font-family="'JetBrains Mono',monospace" font-size="8.5" fill="#334155" letter-spacing="2" font-weight="700">REQUEST FLOWS</text>
<!-- ── FLOW 1: CLI ── -->
<text x="30" y="366" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#475569">CLI path — developer · agent · CI</text>
<!-- node helper: rounded rect + label + sublabel -->
<!-- Node: Actor -->
<rect x="30" y="372" width="100" height="36" rx="4" fill="#60a5fa" fill-opacity="0.1" stroke="#60a5fa" stroke-opacity="0.5" stroke-width="1"/>
<text x="80" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#60a5fa" font-weight="700">Actor</text>
<text x="80" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#60a5fa" fill-opacity="0.7">dev · agent · CI</text>
<!-- arrow -->
<line x1="130" y1="390" x2="156" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: ./ontoref -->
<rect x="158" y="372" width="100" height="36" rx="4" fill="#4ade80" fill-opacity="0.1" stroke="#4ade80" stroke-opacity="0.5" stroke-width="1"/>
<text x="208" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#4ade80" font-weight="700">./ontoref</text>
<text x="208" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#4ade80" fill-opacity="0.7">bash wrapper</text>
<!-- arrow + lock label -->
<line x1="258" y1="390" x2="284" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<text x="271" y="387" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#334155">lock</text>
<!-- Node: ontoref.nu -->
<rect x="286" y="372" width="100" height="36" rx="4" fill="#f97316" fill-opacity="0.1" stroke="#f97316" stroke-opacity="0.5" stroke-width="1"/>
<text x="336" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f97316" font-weight="700">ontoref.nu</text>
<text x="336" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#f97316" fill-opacity="0.7">Nu dispatcher</text>
<!-- arrow -->
<line x1="386" y1="390" x2="412" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: module.nu -->
<rect x="414" y="372" width="100" height="36" rx="4" fill="#f97316" fill-opacity="0.1" stroke="#f97316" stroke-opacity="0.5" stroke-width="1"/>
<text x="464" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f97316" font-weight="700">module.nu</text>
<text x="464" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#f97316" fill-opacity="0.7">adr · backlog · etc</text>
<!-- arrow + nickel label -->
<line x1="514" y1="390" x2="540" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<text x="527" y="387" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#334155">export</text>
<!-- Node: .ontology/ -->
<rect x="542" y="372" width="100" height="36" rx="4" fill="#c084fc" fill-opacity="0.1" stroke="#c084fc" stroke-opacity="0.5" stroke-width="1"/>
<text x="592" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#c084fc" font-weight="700">.ontology/</text>
<text x="592" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#c084fc" fill-opacity="0.7">nickel export</text>
<!-- arrow -->
<line x1="642" y1="390" x2="668" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: reflection/ -->
<rect x="670" y="372" width="100" height="36" rx="4" fill="#60a5fa" fill-opacity="0.1" stroke="#60a5fa" stroke-opacity="0.5" stroke-width="1"/>
<text x="720" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#60a5fa" font-weight="700">reflection/</text>
<text x="720" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#60a5fa" fill-opacity="0.7">qa.ncl · backlog</text>
<!-- daemon optional branch (dashed, from ontoref.nu node bottom) -->
<line x1="336" y1="408" x2="336" y2="420" stroke="#22d3ee" stroke-opacity="0.35" stroke-width="0.8" stroke-dasharray="3 2"/>
<rect x="286" y="420" width="100" height="22" rx="3" fill="#22d3ee" fill-opacity="0.07" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.7"/>
<text x="336" y="435" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#22d3ee" fill-opacity="0.7">daemon? (optional)</text>
<!-- ── FLOW 2: MCP / AI AGENT ── -->
<text x="30" y="462" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#475569">MCP path — AI agent · Claude Code · any MCP client</text>
<!-- Node: AI Agent -->
<rect x="30" y="468" width="100" height="36" rx="4" fill="#E8A838" fill-opacity="0.1" stroke="#E8A838" stroke-opacity="0.5" stroke-width="1"/>
<text x="80" y="482" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#E8A838" font-weight="700">AI Agent</text>
<text x="80" y="494" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#E8A838" fill-opacity="0.7">Claude · any MCP</text>
<!-- arrow -->
<line x1="130" y1="486" x2="156" y2="486" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: MCP server -->
<rect x="158" y="468" width="110" height="36" rx="4" fill="#22d3ee" fill-opacity="0.1" stroke="#22d3ee" stroke-opacity="0.5" stroke-width="1"/>
<text x="213" y="482" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#22d3ee" font-weight="700">MCP server</text>
<text x="213" y="494" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#22d3ee" fill-opacity="0.7">stdio · HTTP /mcp</text>
<!-- arrow -->
<line x1="268" y1="486" x2="294" y2="486" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: 19 tools -->
<rect x="296" y="468" width="100" height="36" rx="4" fill="#22d3ee" fill-opacity="0.1" stroke="#22d3ee" stroke-opacity="0.5" stroke-width="1"/>
<text x="346" y="482" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#22d3ee" font-weight="700">19 tools</text>
<text x="346" y="494" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#22d3ee" fill-opacity="0.7">read · write · query</text>
<!-- arrow -->
<line x1="396" y1="486" x2="422" y2="486" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: NCL files -->
<rect x="424" y="468" width="100" height="36" rx="4" fill="#60a5fa" fill-opacity="0.1" stroke="#60a5fa" stroke-opacity="0.5" stroke-width="1"/>
<text x="474" y="482" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#60a5fa" font-weight="700">NCL files</text>
<text x="474" y="494" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#60a5fa" fill-opacity="0.7">git-versioned</text>
<!-- ── FLOW 3: PRE-COMMIT BARRIER ── -->
<text x="30" y="524" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#475569">Pre-commit barrier — notification-gated commit path</text>
<!-- Node: git commit -->
<rect x="30" y="530" width="100" height="36" rx="4" fill="#f87171" fill-opacity="0.1" stroke="#f87171" stroke-opacity="0.5" stroke-width="1"/>
<text x="80" y="544" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f87171" font-weight="700">git commit</text>
<text x="80" y="556" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#f87171" fill-opacity="0.7">pre-commit fires</text>
<!-- warn arrow -->
<line x1="130" y1="548" x2="156" y2="548" stroke="#f97316" stroke-opacity="0.7" stroke-width="1" marker-end="url(#ao)"/>
<!-- Node: GET /notifications -->
<rect x="158" y="530" width="120" height="36" rx="4" fill="#22d3ee" fill-opacity="0.1" stroke="#22d3ee" stroke-opacity="0.5" stroke-width="1"/>
<text x="218" y="544" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#22d3ee" font-weight="700">GET /notify</text>
<text x="218" y="556" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#22d3ee" fill-opacity="0.7">/pending?token=X</text>
<!-- arrow -->
<line x1="278" y1="548" x2="304" y2="548" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Diamond: pending? -->
<polygon points="330,530 356,548 330,566 304,548" fill="#f97316" fill-opacity="0.1" stroke="#f97316" stroke-opacity="0.6" stroke-width="1"/>
<text x="330" y="552" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7" fill="#f97316" font-weight="700">pending?</text>
<!-- YES arrow (right) -->
<line x1="356" y1="548" x2="382" y2="548" stroke="#f97316" stroke-opacity="0.7" stroke-width="1" marker-end="url(#ao)"/>
<text x="369" y="545" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#f87171">YES</text>
<!-- Node: BLOCK -->
<rect x="384" y="530" width="100" height="36" rx="4" fill="#f87171" fill-opacity="0.12" stroke="#f87171" stroke-opacity="0.6" stroke-width="1"/>
<text x="434" y="544" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f87171" font-weight="700">BLOCK</text>
<text x="434" y="556" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#f87171" fill-opacity="0.7">until acked in UI</text>
<!-- NO arc (down from diamond then right to pass) -->
<path d="M330,566 Q330,578 345,578 L520,578 Q535,578 535,566 L535,566" fill="none" stroke="#4ade80" stroke-opacity="0.4" stroke-width="0.8" stroke-dasharray="3 2" marker-end="url(#ag)"/>
<text x="432" y="576" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#4ade80" fill-opacity="0.7">NO — daemon unreachable → fail-open</text>
<!-- arrow from BLOCK -->
<line x1="484" y1="548" x2="510" y2="548" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<text x="497" y="545" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#4ade80">acked</text>
<!-- Node: PASS -->
<rect x="512" y="530" width="100" height="36" rx="4" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-opacity="0.6" stroke-width="1"/>
<text x="562" y="544" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#4ade80" font-weight="700">PASS ✓</text>
<text x="562" y="556" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#4ade80" fill-opacity="0.7">commit proceeds</text>
<!-- footer -->
<text x="872" y="574" text-anchor="end" font-family="'JetBrains Mono',monospace" font-size="7" fill="#1e293b">ontoref v0.1.0</text>
</svg>

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1,273 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="578" viewBox="0 0 900 578">
<defs>
<marker id="a" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#64748b"/>
</marker>
<marker id="ab" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#60a5fa"/>
</marker>
<marker id="ao" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#E8A838"/>
</marker>
<marker id="ag" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#4ade80"/>
</marker>
<marker id="ar" markerWidth="7" markerHeight="5" refX="6" refY="2.5" orient="auto">
<polygon points="0 0,7 2.5,0 5" fill="#f87171"/>
</marker>
</defs>
<!-- Background -->
<rect width="900" height="578" fill="#f8fafc"/>
<!-- ══════════════════════════════════════════════════════════════
SECTION 1 — PROTOCOL LAYERS
══════════════════════════════════════════════════════════════ -->
<!-- section label -->
<text x="30" y="20" font-family="'JetBrains Mono',monospace" font-size="8.5" fill="#94a3b8" letter-spacing="2" font-weight="700">PROTOCOL LAYERS</text>
<!-- Left connector spine (dashed) -->
<line x1="31.5" y1="30" x2="31.5" y2="298" stroke="#cbd5e1" stroke-width="1.5" stroke-dasharray="3 3"/>
<!-- ── L1: DECLARATIVE / NICKEL ── y=30 -->
<rect x="28" y="30" width="844" height="40" rx="3" fill="#60a5fa" fill-opacity="0.07" stroke="#60a5fa" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="30" width="3" height="40" rx="1" fill="#60a5fa"/>
<text x="40" y="46" font-family="'JetBrains Mono',monospace" font-size="8" fill="#60a5fa" font-weight="700" letter-spacing="1.5">DECLARATIVE · NICKEL</text>
<text x="40" y="60" font-family="'JetBrains Mono',monospace" font-size="7" fill="#60a5fa" fill-opacity="0.6">type-safe contracts · fails at definition time</text>
<!-- chips -->
<rect x="222" y="36" width="52" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="248" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">.ontology/</text>
<rect x="280" y="36" width="46" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="303" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">adrs/*.ncl</text>
<rect x="332" y="36" width="56" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="360" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">modes/*.ncl</text>
<rect x="394" y="36" width="56" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="422" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">schemas/*.ncl</text>
<rect x="456" y="36" width="40" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="476" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">qa.ncl</text>
<rect x="502" y="36" width="56" height="16" rx="3" fill="#60a5fa" fill-opacity="0.12" stroke="#60a5fa" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="530" y="47" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#60a5fa">config.ncl</text>
<!-- inter-layer arrow + label -->
<line x1="31.5" y1="70" x2="31.5" y2="77" stroke="#60a5fa" stroke-opacity="0.4" stroke-width="1" marker-end="url(#ab)"/>
<text x="42" y="76" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#94a3b8" letter-spacing="0.5">nickel export → JSON</text>
<!-- ── L2: KNOWLEDGE GRAPH ── y=80 -->
<rect x="28" y="80" width="844" height="40" rx="3" fill="#c084fc" fill-opacity="0.07" stroke="#c084fc" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="80" width="3" height="40" rx="1" fill="#c084fc"/>
<text x="40" y="96" font-family="'JetBrains Mono',monospace" font-size="8" fill="#c084fc" font-weight="700" letter-spacing="1.5">KNOWLEDGE GRAPH · .ontology/</text>
<text x="40" y="110" font-family="'JetBrains Mono',monospace" font-size="7" fill="#c084fc" fill-opacity="0.6">self-describing · actor-agnostic · machine-queryable</text>
<!-- chips -->
<rect x="222" y="86" width="42" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="243" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">axioms</text>
<rect x="270" y="86" width="46" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="293" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">tensions</text>
<rect x="322" y="86" width="52" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="348" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">practices</text>
<rect x="380" y="86" width="62" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="411" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">nodes + edges</text>
<rect x="448" y="86" width="50" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="473" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">invariants</text>
<rect x="504" y="86" width="34" height="16" rx="3" fill="#c084fc" fill-opacity="0.12" stroke="#c084fc" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="521" y="97" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#c084fc">gates</text>
<!-- inter-layer arrow -->
<line x1="31.5" y1="120" x2="31.5" y2="127" stroke="#f97316" stroke-opacity="0.4" stroke-width="1" marker-end="url(#ao)"/>
<text x="42" y="126" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#94a3b8" letter-spacing="0.5">reads KG via nickel export</text>
<!-- ── L3: OPERATIONAL / NUSHELL ── y=130 -->
<rect x="28" y="130" width="844" height="40" rx="3" fill="#f97316" fill-opacity="0.07" stroke="#f97316" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="130" width="3" height="40" rx="1" fill="#f97316"/>
<text x="40" y="146" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f97316" font-weight="700" letter-spacing="1.5">OPERATIONAL · NUSHELL</text>
<text x="40" y="160" font-family="'JetBrains Mono',monospace" font-size="7" fill="#f97316" fill-opacity="0.6">16 modules · DAG-validated modes · typed pipelines</text>
<!-- chips -->
<rect x="222" y="136" width="40" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="242" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">adr.nu</text>
<rect x="268" y="136" width="52" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="294" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">backlog.nu</text>
<rect x="326" y="136" width="54" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="353" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">describe.nu</text>
<rect x="386" y="136" width="44" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="408" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">sync.nu</text>
<rect x="436" y="136" width="46" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="459" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">coder.nu</text>
<rect x="488" y="136" width="54" height="16" rx="3" fill="#f97316" fill-opacity="0.12" stroke="#f97316" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="515" y="147" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#f97316">+11 modules</text>
<!-- inter-layer arrow -->
<line x1="31.5" y1="170" x2="31.5" y2="177" stroke="#4ade80" stroke-opacity="0.4" stroke-width="1" marker-end="url(#ag)"/>
<text x="42" y="176" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#94a3b8" letter-spacing="0.5">dispatches command + actor</text>
<!-- ── L4: ENTRY POINT / BASH→NU ── y=180 -->
<rect x="28" y="180" width="844" height="40" rx="3" fill="#4ade80" fill-opacity="0.07" stroke="#4ade80" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="180" width="3" height="40" rx="1" fill="#4ade80"/>
<text x="40" y="196" font-family="'JetBrains Mono',monospace" font-size="8" fill="#4ade80" font-weight="700" letter-spacing="1.5">ENTRY POINT · BASH → NU</text>
<text x="40" y="210" font-family="'JetBrains Mono',monospace" font-size="7" fill="#4ade80" fill-opacity="0.6">actor detect · advisory lock · NICKEL_IMPORT_PATH</text>
<!-- chips -->
<rect x="222" y="186" width="52" height="16" rx="3" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="248" y="197" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#4ade80">./ontoref</text>
<rect x="280" y="186" width="62" height="16" rx="3" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="311" y="197" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#4ade80">actor detect</text>
<rect x="348" y="186" width="64" height="16" rx="3" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="380" y="197" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#4ade80">advisory lock</text>
<rect x="418" y="186" width="62" height="16" rx="3" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="449" y="197" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#4ade80">→ ontoref.nu</text>
<!-- inter-layer arrow -->
<line x1="31.5" y1="220" x2="31.5" y2="227" stroke="#22d3ee" stroke-opacity="0.4" stroke-width="1" marker-end="url(#a)"/>
<text x="42" y="226" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#94a3b8" letter-spacing="0.5">optional · caches · serves · never required</text>
<!-- ── L5: RUNTIME / RUST + axum ── y=230 -->
<rect x="28" y="230" width="844" height="40" rx="3" fill="#22d3ee" fill-opacity="0.07" stroke="#22d3ee" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="230" width="3" height="40" rx="1" fill="#22d3ee"/>
<text x="40" y="246" font-family="'JetBrains Mono',monospace" font-size="8" fill="#22d3ee" font-weight="700" letter-spacing="1.5">RUNTIME · RUST + axum (optional daemon)</text>
<text x="40" y="260" font-family="'JetBrains Mono',monospace" font-size="7" fill="#22d3ee" fill-opacity="0.6">10 UI pages · 19 MCP tools · actor registry · search · SurrealDB?</text>
<!-- chips -->
<rect x="222" y="236" width="76" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="260" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">ontoref-daemon</text>
<rect x="304" y="236" width="56" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="332" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">axum HTTP</text>
<rect x="366" y="236" width="46" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="389" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">MCP ×19</text>
<rect x="418" y="236" width="46" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="441" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">DashMap</text>
<rect x="470" y="236" width="38" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="489" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">search</text>
<rect x="514" y="236" width="56" height="16" rx="3" fill="#22d3ee" fill-opacity="0.12" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="542" y="247" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#22d3ee">SurrealDB?</text>
<!-- inter-layer arrow -->
<line x1="31.5" y1="270" x2="31.5" y2="277" stroke="#facc15" stroke-opacity="0.4" stroke-width="1" marker-end="url(#a)"/>
<text x="42" y="276" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#94a3b8" letter-spacing="0.5">configures · ONTOREF_PROJECT_ROOT</text>
<!-- ── L6: ADOPTION ── y=280 -->
<rect x="28" y="280" width="844" height="40" rx="3" fill="#facc15" fill-opacity="0.07" stroke="#facc15" stroke-opacity="0.25" stroke-width="0.8"/>
<rect x="28" y="280" width="3" height="40" rx="1" fill="#facc15"/>
<text x="40" y="296" font-family="'JetBrains Mono',monospace" font-size="8" fill="#facc15" font-weight="700" letter-spacing="1.5">ADOPTION · PER-PROJECT</text>
<text x="40" y="310" font-family="'JetBrains Mono',monospace" font-size="7" fill="#facc15" fill-opacity="0.6">templates/ · one ontoref checkout · zero lock-in</text>
<!-- chips -->
<rect x="222" y="286" width="56" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="250" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">templates/</text>
<rect x="284" y="286" width="74" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="321" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">adopt_ontoref</text>
<rect x="364" y="286" width="108" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="418" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">ONTOREF_PROJECT_ROOT</text>
<rect x="478" y="286" width="62" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="509" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">zero lock-in</text>
<rect x="546" y="286" width="58" height="16" rx="3" fill="#facc15" fill-opacity="0.12" stroke="#facc15" stroke-width="0.5" stroke-opacity="0.4"/>
<text x="575" y="297" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#facc15">plain NCL</text>
<!-- ══════════════════════════════════════════════════════════════
DIVIDER
══════════════════════════════════════════════════════════════ -->
<line x1="28" y1="334" x2="872" y2="334" stroke="#cbd5e1" stroke-width="1"/>
<!-- ══════════════════════════════════════════════════════════════
SECTION 2 — REQUEST FLOWS
══════════════════════════════════════════════════════════════ -->
<text x="30" y="348" font-family="'JetBrains Mono',monospace" font-size="8.5" fill="#94a3b8" letter-spacing="2" font-weight="700">REQUEST FLOWS</text>
<!-- ── FLOW 1: CLI ── -->
<text x="30" y="366" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#64748b">CLI path — developer · agent · CI</text>
<!-- node helper: rounded rect + label + sublabel -->
<!-- Node: Actor -->
<rect x="30" y="372" width="100" height="36" rx="4" fill="#60a5fa" fill-opacity="0.1" stroke="#60a5fa" stroke-opacity="0.5" stroke-width="1"/>
<text x="80" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#60a5fa" font-weight="700">Actor</text>
<text x="80" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#60a5fa" fill-opacity="0.7">dev · agent · CI</text>
<!-- arrow -->
<line x1="130" y1="390" x2="156" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: ./ontoref -->
<rect x="158" y="372" width="100" height="36" rx="4" fill="#4ade80" fill-opacity="0.1" stroke="#4ade80" stroke-opacity="0.5" stroke-width="1"/>
<text x="208" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#4ade80" font-weight="700">./ontoref</text>
<text x="208" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#4ade80" fill-opacity="0.7">bash wrapper</text>
<!-- arrow + lock label -->
<line x1="258" y1="390" x2="284" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<text x="271" y="387" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#94a3b8">lock</text>
<!-- Node: ontoref.nu -->
<rect x="286" y="372" width="100" height="36" rx="4" fill="#f97316" fill-opacity="0.1" stroke="#f97316" stroke-opacity="0.5" stroke-width="1"/>
<text x="336" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f97316" font-weight="700">ontoref.nu</text>
<text x="336" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#f97316" fill-opacity="0.7">Nu dispatcher</text>
<!-- arrow -->
<line x1="386" y1="390" x2="412" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: module.nu -->
<rect x="414" y="372" width="100" height="36" rx="4" fill="#f97316" fill-opacity="0.1" stroke="#f97316" stroke-opacity="0.5" stroke-width="1"/>
<text x="464" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f97316" font-weight="700">module.nu</text>
<text x="464" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#f97316" fill-opacity="0.7">adr · backlog · etc</text>
<!-- arrow + nickel label -->
<line x1="514" y1="390" x2="540" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<text x="527" y="387" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#94a3b8">export</text>
<!-- Node: .ontology/ -->
<rect x="542" y="372" width="100" height="36" rx="4" fill="#c084fc" fill-opacity="0.1" stroke="#c084fc" stroke-opacity="0.5" stroke-width="1"/>
<text x="592" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#c084fc" font-weight="700">.ontology/</text>
<text x="592" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#c084fc" fill-opacity="0.7">nickel export</text>
<!-- arrow -->
<line x1="642" y1="390" x2="668" y2="390" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: reflection/ -->
<rect x="670" y="372" width="100" height="36" rx="4" fill="#60a5fa" fill-opacity="0.1" stroke="#60a5fa" stroke-opacity="0.5" stroke-width="1"/>
<text x="720" y="386" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#60a5fa" font-weight="700">reflection/</text>
<text x="720" y="398" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#60a5fa" fill-opacity="0.7">qa.ncl · backlog</text>
<!-- daemon optional branch (dashed, from ontoref.nu node bottom) -->
<line x1="336" y1="408" x2="336" y2="420" stroke="#22d3ee" stroke-opacity="0.35" stroke-width="0.8" stroke-dasharray="3 2"/>
<rect x="286" y="420" width="100" height="22" rx="3" fill="#22d3ee" fill-opacity="0.07" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.7"/>
<text x="336" y="435" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#22d3ee" fill-opacity="0.7">daemon? (optional)</text>
<!-- ── FLOW 2: MCP / AI AGENT ── -->
<text x="30" y="462" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#64748b">MCP path — AI agent · Claude Code · any MCP client</text>
<!-- Node: AI Agent -->
<rect x="30" y="468" width="100" height="36" rx="4" fill="#E8A838" fill-opacity="0.1" stroke="#E8A838" stroke-opacity="0.5" stroke-width="1"/>
<text x="80" y="482" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#E8A838" font-weight="700">AI Agent</text>
<text x="80" y="494" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#E8A838" fill-opacity="0.7">Claude · any MCP</text>
<!-- arrow -->
<line x1="130" y1="486" x2="156" y2="486" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: MCP server -->
<rect x="158" y="468" width="110" height="36" rx="4" fill="#22d3ee" fill-opacity="0.1" stroke="#22d3ee" stroke-opacity="0.5" stroke-width="1"/>
<text x="213" y="482" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#22d3ee" font-weight="700">MCP server</text>
<text x="213" y="494" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#22d3ee" fill-opacity="0.7">stdio · HTTP /mcp</text>
<!-- arrow -->
<line x1="268" y1="486" x2="294" y2="486" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: 19 tools -->
<rect x="296" y="468" width="100" height="36" rx="4" fill="#22d3ee" fill-opacity="0.1" stroke="#22d3ee" stroke-opacity="0.5" stroke-width="1"/>
<text x="346" y="482" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#22d3ee" font-weight="700">19 tools</text>
<text x="346" y="494" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#22d3ee" fill-opacity="0.7">read · write · query</text>
<!-- arrow -->
<line x1="396" y1="486" x2="422" y2="486" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Node: NCL files -->
<rect x="424" y="468" width="100" height="36" rx="4" fill="#60a5fa" fill-opacity="0.1" stroke="#60a5fa" stroke-opacity="0.5" stroke-width="1"/>
<text x="474" y="482" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#60a5fa" font-weight="700">NCL files</text>
<text x="474" y="494" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#60a5fa" fill-opacity="0.7">git-versioned</text>
<!-- ── FLOW 3: PRE-COMMIT BARRIER ── -->
<text x="30" y="524" font-family="'JetBrains Mono',monospace" font-size="7.5" fill="#64748b">Pre-commit barrier — notification-gated commit path</text>
<!-- Node: git commit -->
<rect x="30" y="530" width="100" height="36" rx="4" fill="#f87171" fill-opacity="0.1" stroke="#f87171" stroke-opacity="0.5" stroke-width="1"/>
<text x="80" y="544" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f87171" font-weight="700">git commit</text>
<text x="80" y="556" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#f87171" fill-opacity="0.7">pre-commit fires</text>
<!-- warn arrow -->
<line x1="130" y1="548" x2="156" y2="548" stroke="#f97316" stroke-opacity="0.7" stroke-width="1" marker-end="url(#ao)"/>
<!-- Node: GET /notifications -->
<rect x="158" y="530" width="120" height="36" rx="4" fill="#22d3ee" fill-opacity="0.1" stroke="#22d3ee" stroke-opacity="0.5" stroke-width="1"/>
<text x="218" y="544" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#22d3ee" font-weight="700">GET /notify</text>
<text x="218" y="556" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#22d3ee" fill-opacity="0.7">/pending?token=X</text>
<!-- arrow -->
<line x1="278" y1="548" x2="304" y2="548" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<!-- Diamond: pending? -->
<polygon points="330,530 356,548 330,566 304,548" fill="#f97316" fill-opacity="0.1" stroke="#f97316" stroke-opacity="0.6" stroke-width="1"/>
<text x="330" y="552" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="7" fill="#f97316" font-weight="700">pending?</text>
<!-- YES arrow (right) -->
<line x1="356" y1="548" x2="382" y2="548" stroke="#f97316" stroke-opacity="0.7" stroke-width="1" marker-end="url(#ao)"/>
<text x="369" y="545" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#f87171">YES</text>
<!-- Node: BLOCK -->
<rect x="384" y="530" width="100" height="36" rx="4" fill="#f87171" fill-opacity="0.12" stroke="#f87171" stroke-opacity="0.6" stroke-width="1"/>
<text x="434" y="544" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#f87171" font-weight="700">BLOCK</text>
<text x="434" y="556" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#f87171" fill-opacity="0.7">until acked in UI</text>
<!-- NO arc (down from diamond then right to pass) -->
<path d="M330,566 Q330,578 345,578 L520,578 Q535,578 535,566 L535,566" fill="none" stroke="#4ade80" stroke-opacity="0.4" stroke-width="0.8" stroke-dasharray="3 2" marker-end="url(#ag)"/>
<text x="432" y="576" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#4ade80" fill-opacity="0.7">NO — daemon unreachable → fail-open</text>
<!-- arrow from BLOCK -->
<line x1="484" y1="548" x2="510" y2="548" stroke="#475569" stroke-width="1" marker-end="url(#a)"/>
<text x="497" y="545" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="5.5" fill="#4ade80">acked</text>
<!-- Node: PASS -->
<rect x="512" y="530" width="100" height="36" rx="4" fill="#4ade80" fill-opacity="0.12" stroke="#4ade80" stroke-opacity="0.6" stroke-width="1"/>
<text x="562" y="544" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="8" fill="#4ade80" font-weight="700">PASS ✓</text>
<text x="562" y="556" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="#4ade80" fill-opacity="0.7">commit proceeds</text>
<!-- footer -->
<text x="872" y="574" text-anchor="end" font-family="'JetBrains Mono',monospace" font-size="7" fill="#94a3b8">ontoref v0.1.0</text>
</svg>

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 70 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -0,0 +1,253 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 780" font-family="ui-monospace, 'JetBrains Mono', monospace">
<defs>
<marker id="af" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#94a3b8"/>
</marker>
<marker id="ag" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#34d399"/>
</marker>
<marker id="ar" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#f87171"/>
</marker>
<marker id="aa" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#fbbf24"/>
</marker>
<marker id="av" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#a78bfa"/>
</marker>
<marker id="ab" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#3b82f6"/>
</marker>
</defs>
<!-- background -->
<rect width="1080" height="780" fill="#0f172a"/>
<!-- title -->
<text x="540" y="28" text-anchor="middle" font-size="16" font-weight="700" fill="#f1f5f9">stratumiops — Operation Flow</text>
<text x="540" y="46" text-anchor="middle" font-size="10" fill="#475569">two main flows: knowledge registration · config sealed state — actor-aware, advisory locking</text>
<!-- ═══ SECTION DIVIDER ══════════════════════════════════════════════ -->
<!-- Left flow label -->
<rect x="12" y="56" width="518" height="18" rx="4" fill="#172033" stroke="#34d399" stroke-width="1"/>
<text x="271" y="68" text-anchor="middle" font-size="9" font-weight="600" fill="#34d399" letter-spacing="1">FLOW A — KNOWLEDGE REGISTRATION (stratum register)</text>
<!-- Right flow label -->
<rect x="542" y="56" width="526" height="18" rx="4" fill="#172033" stroke="#3b82f6" stroke-width="1"/>
<text x="805" y="68" text-anchor="middle" font-size="9" font-weight="600" fill="#60a5fa" letter-spacing="1">FLOW B — CONFIG SEALED STATE (stratum config apply)</text>
<!-- ═══════════════════════════════════════════════════════════════════
FLOW A: REGISTER
Steps: actor → lock changelog → form → outputs → release
═══════════════════════════════════════════════════════════════════ -->
<!-- A1: Actor / trigger -->
<rect x="16" y="82" width="148" height="64" rx="6" fill="#1e293b" stroke="#94a3b8" stroke-width="1"/>
<text x="90" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#cbd5e1" letter-spacing="1">ACTOR</text>
<text x="90" y="112" text-anchor="middle" font-size="8" fill="#94a3b8">developer (TTY)</text>
<text x="90" y="124" text-anchor="middle" font-size="8" fill="#94a3b8">agent (piped)</text>
<text x="90" y="136" text-anchor="middle" font-size="8" fill="#94a3b8">ci / admin</text>
<line x1="164" y1="114" x2="182" y2="114" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#af)"/>
<!-- A2: Lock acquire -->
<rect x="184" y="82" width="152" height="64" rx="6" fill="#3b0000" stroke="#f87171" stroke-width="1"/>
<text x="260" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#fca5a5" letter-spacing="1">LOCK ACQUIRE</text>
<text x="260" y="112" text-anchor="middle" font-size="8" fill="#fda4af">mkdir .stratum/locks/changelog</text>
<text x="260" y="124" text-anchor="middle" font-size="8" fill="#fda4af">write PID to lock dir</text>
<text x="260" y="136" text-anchor="middle" font-size="8" fill="#64748b">stale? kill -0 PID → remove</text>
<line x1="336" y1="114" x2="354" y2="114" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#af)"/>
<!-- A3: Form (actor-branching) -->
<rect x="356" y="82" width="170" height="64" rx="6" fill="#2a1a00" stroke="#fbbf24" stroke-width="1"/>
<text x="441" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#fde68a" letter-spacing="1">FORM (actor-aware)</text>
<text x="441" y="112" text-anchor="middle" font-size="8" fill="#fcd34d">developer → typedialog UI</text>
<text x="441" y="124" text-anchor="middle" font-size="8" fill="#fcd34d">agent/ci → nickel-export + tera</text>
<text x="441" y="136" text-anchor="middle" font-size="8" fill="#64748b">forms/*.ncl drives field layout</text>
<line x1="526" y1="114" x2="526" y2="160" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#aa)"/>
<text x="530" y="140" font-size="7" fill="#fbbf24">filled</text>
<!-- A4: Outputs fan-out -->
<rect x="356" y="162" width="170" height="100" rx="6" fill="#0a2e1a" stroke="#34d399" stroke-width="1"/>
<text x="441" y="178" text-anchor="middle" font-size="9" font-weight="600" fill="#4ade80" letter-spacing="1">REGISTER OUTPUTS</text>
<text x="441" y="193" text-anchor="middle" font-size="8" fill="#86efac">CHANGELOG append (locked)</text>
<text x="441" y="206" text-anchor="middle" font-size="8" fill="#86efac">ADR hint → adrs/adr-NNN.ncl</text>
<text x="441" y="219" text-anchor="middle" font-size="8" fill="#86efac">ontology patch → .ontology/</text>
<text x="441" y="232" text-anchor="middle" font-size="8" fill="#86efac">mode stub → reflection/modes/</text>
<text x="441" y="248" text-anchor="middle" font-size="8" fill="#64748b">optional: trigger config apply</text>
<!-- A5: Lock release -->
<rect x="184" y="178" width="152" height="52" rx="6" fill="#3b0000" stroke="#f87171" stroke-width="1"/>
<text x="260" y="196" text-anchor="middle" font-size="9" font-weight="600" fill="#fca5a5" letter-spacing="1">LOCK RELEASE</text>
<text x="260" y="210" text-anchor="middle" font-size="8" fill="#fda4af">rm -rf .stratum/locks/changelog</text>
<text x="260" y="222" text-anchor="middle" font-size="8" fill="#64748b">triggered by EXIT trap</text>
<!-- outputs → lock release arrow -->
<line x1="356" y1="212" x2="336" y2="212" stroke="#f87171" stroke-width="1.5" marker-end="url(#ar)"/>
<!-- ─── separator A4 → gate validation ─── -->
<line x1="441" y1="262" x2="441" y2="310" stroke="#34d399" stroke-width="1" marker-end="url(#ag)"/>
<!-- A6: Gate/ADR validation -->
<rect x="16" y="312" width="510" height="68" rx="6" fill="#1e293b" stroke="#fbbf24" stroke-width="1"/>
<text x="271" y="330" text-anchor="middle" font-size="9" font-weight="600" fill="#fde68a" letter-spacing="1">ADR CONSTRAINT VALIDATION (stratum adr validate)</text>
<text x="271" y="346" text-anchor="middle" font-size="8" fill="#fcd34d">nickel export adrs/*.ncl | where status == Accepted | get constraints | where severity == Hard</text>
<text x="271" y="360" text-anchor="middle" font-size="8" fill="#fcd34d">for each: nu -c check_hint → exit 0 + no stdout = pass</text>
<text x="271" y="372" text-anchor="middle" font-size="8" fill="#64748b">^nickel export (not plugin cache) — always fresh · Superseded ADRs excluded</text>
<!-- ═══════════════════════════════════════════════════════════════════
FLOW B: CONFIG APPLY
═══════════════════════════════════════════════════════════════════ -->
<!-- vertical separator line -->
<line x1="538" y1="56" x2="538" y2="390" stroke="#334155" stroke-width="1" stroke-dasharray="4,3"/>
<!-- B1: Actor / trigger -->
<rect x="546" y="82" width="148" height="64" rx="6" fill="#1e293b" stroke="#94a3b8" stroke-width="1"/>
<text x="620" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#cbd5e1" letter-spacing="1">ACTOR</text>
<text x="620" y="112" text-anchor="middle" font-size="8" fill="#94a3b8">developer (TTY)</text>
<text x="620" y="124" text-anchor="middle" font-size="8" fill="#94a3b8">agent (piped)</text>
<text x="620" y="136" text-anchor="middle" font-size="8" fill="#94a3b8">ci / admin</text>
<line x1="694" y1="114" x2="712" y2="114" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#af)"/>
<!-- B2: Lock acquire (manifest) -->
<rect x="714" y="82" width="152" height="64" rx="6" fill="#3b0000" stroke="#f87171" stroke-width="1"/>
<text x="790" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#fca5a5" letter-spacing="1">LOCK ACQUIRE</text>
<text x="790" y="112" text-anchor="middle" font-size="8" fill="#fda4af">mkdir .stratum/locks/manifest</text>
<text x="790" y="124" text-anchor="middle" font-size="8" fill="#fda4af">write PID to lock dir</text>
<text x="790" y="136" text-anchor="middle" font-size="8" fill="#64748b">stale? kill -0 PID → remove</text>
<line x1="866" y1="114" x2="884" y2="114" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#af)"/>
<!-- B3: nickel export + hash -->
<rect x="886" y="82" width="178" height="64" rx="6" fill="#172554" stroke="#3b82f6" stroke-width="1"/>
<text x="975" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#93c5fd" letter-spacing="1">EXPORT + HASH</text>
<text x="975" y="112" text-anchor="middle" font-size="8" fill="#7dd3fc">nickel export &lt;profile&gt;.ncl</text>
<text x="975" y="124" text-anchor="middle" font-size="8" fill="#7dd3fc">sha256(export) → seal.hash</text>
<text x="975" y="136" text-anchor="middle" font-size="8" fill="#64748b">sha256(snapshot) → snapshot_hash</text>
<line x1="975" y1="146" x2="975" y2="162" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#ab)"/>
<!-- B4: write history entry -->
<rect x="886" y="164" width="178" height="80" rx="6" fill="#172554" stroke="#3b82f6" stroke-width="1"/>
<text x="975" y="180" text-anchor="middle" font-size="9" font-weight="600" fill="#93c5fd" letter-spacing="1">HISTORY ENTRY</text>
<text x="975" y="195" text-anchor="middle" font-size="8" fill="#7dd3fc">cfg-&lt;YYYYMMDDTHHmmSS&gt;-&lt;actor&gt;.ncl</text>
<text x="975" y="208" text-anchor="middle" font-size="8" fill="#7dd3fc">ConfigState record (immutable)</text>
<text x="975" y="221" text-anchor="middle" font-size="8" fill="#7dd3fc">seal.hash + snapshot_hash</text>
<text x="975" y="234" text-anchor="middle" font-size="8" fill="#7dd3fc">supersedes: prev entry | null</text>
<line x1="975" y1="244" x2="975" y2="260" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#ab)"/>
<!-- B5: patch manifest -->
<rect x="886" y="262" width="178" height="60" rx="6" fill="#172554" stroke="#3b82f6" stroke-width="1"/>
<text x="975" y="278" text-anchor="middle" font-size="9" font-weight="600" fill="#93c5fd" letter-spacing="1">PATCH MANIFEST</text>
<text x="975" y="293" text-anchor="middle" font-size="8" fill="#7dd3fc">manifest.ncl → active[&lt;profile&gt;]</text>
<text x="975" y="306" text-anchor="middle" font-size="8" fill="#7dd3fc">= &lt;new cfg entry ref&gt;</text>
<text x="975" y="318" text-anchor="middle" font-size="8" fill="#64748b">under manifest lock</text>
<!-- B6: Lock release -->
<rect x="714" y="178" width="152" height="52" rx="6" fill="#3b0000" stroke="#f87171" stroke-width="1"/>
<text x="790" y="196" text-anchor="middle" font-size="9" font-weight="600" fill="#fca5a5" letter-spacing="1">LOCK RELEASE</text>
<text x="790" y="210" text-anchor="middle" font-size="8" fill="#fda4af">rm -rf .stratum/locks/manifest</text>
<text x="790" y="222" text-anchor="middle" font-size="8" fill="#64748b">triggered by EXIT trap</text>
<!-- patch manifest → lock release -->
<line x1="886" y1="292" x2="866" y2="292" stroke="#f87171" stroke-width="1.5" marker-end="url(#ar)"/>
<!-- B7: audit (drift check) sits below -->
<line x1="975" y1="322" x2="975" y2="370" stroke="#3b82f6" stroke-width="1" marker-end="url(#ab)"/>
<!-- ─── B vertical: audit box ─── -->
<rect x="546" y="312" width="510" height="68" rx="6" fill="#1e293b" stroke="#3b82f6" stroke-width="1"/>
<text x="801" y="330" text-anchor="middle" font-size="9" font-weight="600" fill="#93c5fd" letter-spacing="1">DRIFT AUDIT (stratum config audit)</text>
<text x="801" y="346" text-anchor="middle" font-size="8" fill="#7dd3fc">for each profile in manifest: sha256(live profile.ncl) == manifest.active[p].seal.hash</text>
<text x="801" y="360" text-anchor="middle" font-size="8" fill="#7dd3fc">match → clean · mismatch → drift (warn actor, optionally block)</text>
<text x="801" y="372" text-anchor="middle" font-size="8" fill="#64748b">read-only; no lock acquired · rollback: verify snapshot_hash → restore → new apply (forward operation)</text>
<!-- ═══ SUMMARY / EXAMPLES ════════════════════════════════════════════ -->
<rect x="12" y="398" width="1056" height="90" rx="8" fill="#161b26" stroke="#334155" stroke-width="1"/>
<text x="540" y="418" text-anchor="middle" font-size="10" font-weight="700" fill="#94a3b8">actor flows through both paths automatically — locking is per-resource, not global</text>
<rect x="28" y="428" width="230" height="50" rx="5" fill="#0a2e1a" stroke="#166534" stroke-width="1"/>
<text x="143" y="444" text-anchor="middle" font-size="8" fill="#4ade80">stratum.sh register</text>
<text x="143" y="457" text-anchor="middle" font-size="8" fill="#86efac">STRATUM_ACTOR=developer (TTY)</text>
<text x="143" y="470" text-anchor="middle" font-size="8" fill="#64748b">typedialog form → CHANGELOG locked</text>
<rect x="272" y="428" width="230" height="50" rx="5" fill="#2a1a00" stroke="#92400e" stroke-width="1"/>
<text x="387" y="444" text-anchor="middle" font-size="8" fill="#fcd34d">stratum.sh config apply prod</text>
<text x="387" y="457" text-anchor="middle" font-size="8" fill="#fde68a">STRATUM_ACTOR=ci</text>
<text x="387" y="470" text-anchor="middle" font-size="8" fill="#64748b">manifest lock → cfg-ts-ci.ncl written</text>
<rect x="516" y="428" width="230" height="50" rx="5" fill="#1e1b4b" stroke="#4338ca" stroke-width="1"/>
<text x="631" y="444" text-anchor="middle" font-size="8" fill="#a78bfa">stratum.sh config audit</text>
<text x="631" y="457" text-anchor="middle" font-size="8" fill="#818cf8">any actor, no lock needed</text>
<text x="631" y="470" text-anchor="middle" font-size="8" fill="#64748b">sha256 compare live vs seal.hash</text>
<rect x="760" y="428" width="296" height="50" rx="5" fill="#172554" stroke="#1d4ed8" stroke-width="1"/>
<text x="908" y="444" text-anchor="middle" font-size="8" fill="#93c5fd">stratum.sh config rollback &lt;ts&gt;</text>
<text x="908" y="457" text-anchor="middle" font-size="8" fill="#7dd3fc">verify snapshot_hash → restore → new apply</text>
<text x="908" y="470" text-anchor="middle" font-size="8" fill="#64748b">rollback is always a forward operation</text>
<!-- ═══ LEGEND ═══════════════════════════════════════════════════════ -->
<rect x="12" y="502" width="1056" height="58" rx="8" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="40" y="522" font-size="9" fill="#94a3b8">Legend:</text>
<line x1="100" y1="518" x2="132" y2="518" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#af)"/>
<text x="138" y="522" font-size="8" fill="#64748b">main flow</text>
<line x1="220" y1="518" x2="252" y2="518" stroke="#34d399" stroke-width="1.5" marker-end="url(#ag)"/>
<text x="258" y="522" font-size="8" fill="#64748b">pass / ok</text>
<line x1="340" y1="518" x2="372" y2="518" stroke="#f87171" stroke-width="1.5" marker-end="url(#ar)"/>
<text x="378" y="522" font-size="8" fill="#64748b">error / lock release</text>
<line x1="478" y1="518" x2="510" y2="518" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#aa)"/>
<text x="516" y="522" font-size="8" fill="#64748b">form complete</text>
<line x1="604" y1="518" x2="636" y2="518" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#ab)"/>
<text x="642" y="522" font-size="8" fill="#64748b">config flow (hash/write)</text>
<line x1="748" y1="518" x2="780" y2="518" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#av)"/>
<text x="786" y="522" font-size="8" fill="#64748b">output / audit</text>
<text x="40" y="542" font-size="8" fill="#475569">Lock resources:</text>
<text x="130" y="542" font-size="8" fill="#64748b">changelog (register) · manifest (config apply/rollback) · backlog (backlog done/cancel)</text>
<text x="40" y="555" font-size="8" fill="#475569">History IDs:</text>
<text x="108" y="555" font-size="8" fill="#64748b">cfg-&lt;YYYYMMDDTHHmmSS&gt;-&lt;actor&gt;.ncl — timestamp collision-free, no lock needed for history writes</text>
<!-- ═══ BACKLOG SUB-FLOW ═══════════════════════════════════════════ -->
<rect x="12" y="574" width="1056" height="90" rx="8" fill="#1e293b" stroke="#fbbf24" stroke-width="1"/>
<text x="36" y="592" font-size="9" font-weight="600" fill="#fbbf24" letter-spacing="1">FLOW C — BACKLOG (stratum backlog …)</text>
<rect x="28" y="600" width="170" height="54" rx="5" fill="#1c1917" stroke="#fbbf24" stroke-width="1"/>
<text x="113" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#fde68a">backlog list|show</text>
<text x="113" y="629" text-anchor="middle" font-size="8" fill="#fcd34d">read backlog.ncl (no lock)</text>
<text x="113" y="642" text-anchor="middle" font-size="8" fill="#64748b">nickel export → filter/sort</text>
<line x1="198" y1="627" x2="216" y2="627" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#aa)"/>
<rect x="218" y="600" width="178" height="54" rx="5" fill="#1c1917" stroke="#fbbf24" stroke-width="1"/>
<text x="307" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#fde68a">backlog add</text>
<text x="307" y="629" text-anchor="middle" font-size="8" fill="#fcd34d">no lock (append, unique ID)</text>
<text x="307" y="642" text-anchor="middle" font-size="8" fill="#64748b">typed item: Todo|Bug|Wish|Debt</text>
<line x1="396" y1="627" x2="414" y2="627" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#aa)"/>
<rect x="416" y="600" width="192" height="54" rx="5" fill="#1c1917" stroke="#f87171" stroke-width="1"/>
<text x="512" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#fca5a5">backlog done|cancel</text>
<text x="512" y="629" text-anchor="middle" font-size="8" fill="#fda4af">mkdir lock on backlog resource</text>
<text x="512" y="642" text-anchor="middle" font-size="8" fill="#64748b">mutates status field in backlog.ncl</text>
<line x1="608" y1="627" x2="626" y2="627" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#aa)"/>
<rect x="628" y="600" width="190" height="54" rx="5" fill="#1c1917" stroke="#fbbf24" stroke-width="1"/>
<text x="723" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#fde68a">backlog promote</text>
<text x="723" y="629" text-anchor="middle" font-size="8" fill="#fcd34d">graduates_to: Adr|Mode|StateTransition</text>
<text x="723" y="642" text-anchor="middle" font-size="8" fill="#64748b">triggers adr or mode stub creation</text>
<line x1="818" y1="627" x2="836" y2="627" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#aa)"/>
<rect x="838" y="600" width="216" height="54" rx="5" fill="#1c1917" stroke="#fbbf24" stroke-width="1"/>
<text x="946" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#fde68a">backlog roadmap</text>
<text x="946" y="629" text-anchor="middle" font-size="8" fill="#fcd34d">state.ncl dimensions not-yet-reached</text>
<text x="946" y="642" text-anchor="middle" font-size="8" fill="#64748b">open items by priority (read-only)</text>
<text x="1068" y="770" text-anchor="end" font-size="8" fill="#334155">2026-03</text>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,252 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 780" font-family="ui-monospace, 'JetBrains Mono', monospace">
<defs>
<marker id="af" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#64748b"/>
</marker>
<marker id="ag" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#059669"/>
</marker>
<marker id="ar" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#dc2626"/>
</marker>
<marker id="aa" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#d97706"/>
</marker>
<marker id="av" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#7c3aed"/>
</marker>
<marker id="ab" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#2563eb"/>
</marker>
</defs>
<!-- background -->
<rect width="1080" height="780" fill="#ffffff"/>
<!-- title -->
<text x="540" y="28" text-anchor="middle" font-size="16" font-weight="700" fill="#0f172a">stratumiops — Operation Flow</text>
<text x="540" y="46" text-anchor="middle" font-size="10" fill="#64748b">two main flows: knowledge registration · config sealed state — actor-aware, advisory locking</text>
<!-- ═══ SECTION DIVIDER ══════════════════════════════════════════════ -->
<!-- Left flow label -->
<rect x="12" y="56" width="518" height="18" rx="4" fill="#f0fdf4" stroke="#059669" stroke-width="1"/>
<text x="271" y="68" text-anchor="middle" font-size="9" font-weight="600" fill="#059669" letter-spacing="1">FLOW A — KNOWLEDGE REGISTRATION (stratum register)</text>
<!-- Right flow label -->
<rect x="542" y="56" width="526" height="18" rx="4" fill="#eff6ff" stroke="#2563eb" stroke-width="1"/>
<text x="805" y="68" text-anchor="middle" font-size="9" font-weight="600" fill="#2563eb" letter-spacing="1">FLOW B — CONFIG SEALED STATE (stratum config apply)</text>
<!-- ═══════════════════════════════════════════════════════════════════
FLOW A: REGISTER
═══════════════════════════════════════════════════════════════════ -->
<!-- A1: Actor / trigger -->
<rect x="16" y="82" width="148" height="64" rx="6" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="90" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#334155" letter-spacing="1">ACTOR</text>
<text x="90" y="112" text-anchor="middle" font-size="8" fill="#64748b">developer (TTY)</text>
<text x="90" y="124" text-anchor="middle" font-size="8" fill="#64748b">agent (piped)</text>
<text x="90" y="136" text-anchor="middle" font-size="8" fill="#64748b">ci / admin</text>
<line x1="164" y1="114" x2="182" y2="114" stroke="#64748b" stroke-width="1.5" marker-end="url(#af)"/>
<!-- A2: Lock acquire -->
<rect x="184" y="82" width="152" height="64" rx="6" fill="#fff5f5" stroke="#ef4444" stroke-width="1"/>
<text x="260" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#dc2626" letter-spacing="1">LOCK ACQUIRE</text>
<text x="260" y="112" text-anchor="middle" font-size="8" fill="#be123c">mkdir .stratum/locks/changelog</text>
<text x="260" y="124" text-anchor="middle" font-size="8" fill="#be123c">write PID to lock dir</text>
<text x="260" y="136" text-anchor="middle" font-size="8" fill="#94a3b8">stale? kill -0 PID → remove</text>
<line x1="336" y1="114" x2="354" y2="114" stroke="#64748b" stroke-width="1.5" marker-end="url(#af)"/>
<!-- A3: Form (actor-branching) -->
<rect x="356" y="82" width="170" height="64" rx="6" fill="#fffbeb" stroke="#f59e0b" stroke-width="1"/>
<text x="441" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#92400e" letter-spacing="1">FORM (actor-aware)</text>
<text x="441" y="112" text-anchor="middle" font-size="8" fill="#b45309">developer → typedialog UI</text>
<text x="441" y="124" text-anchor="middle" font-size="8" fill="#b45309">agent/ci → nickel-export + tera</text>
<text x="441" y="136" text-anchor="middle" font-size="8" fill="#94a3b8">forms/*.ncl drives field layout</text>
<line x1="526" y1="114" x2="526" y2="160" stroke="#d97706" stroke-width="1.5" marker-end="url(#aa)"/>
<text x="530" y="140" font-size="7" fill="#d97706">filled</text>
<!-- A4: Outputs fan-out -->
<rect x="356" y="162" width="170" height="100" rx="6" fill="#f0fdf4" stroke="#059669" stroke-width="1"/>
<text x="441" y="178" text-anchor="middle" font-size="9" font-weight="600" fill="#16a34a" letter-spacing="1">REGISTER OUTPUTS</text>
<text x="441" y="193" text-anchor="middle" font-size="8" fill="#166534">CHANGELOG append (locked)</text>
<text x="441" y="206" text-anchor="middle" font-size="8" fill="#166534">ADR hint → adrs/adr-NNN.ncl</text>
<text x="441" y="219" text-anchor="middle" font-size="8" fill="#166534">ontology patch → .ontology/</text>
<text x="441" y="232" text-anchor="middle" font-size="8" fill="#166534">mode stub → reflection/modes/</text>
<text x="441" y="248" text-anchor="middle" font-size="8" fill="#94a3b8">optional: trigger config apply</text>
<!-- A5: Lock release -->
<rect x="184" y="178" width="152" height="52" rx="6" fill="#fff5f5" stroke="#ef4444" stroke-width="1"/>
<text x="260" y="196" text-anchor="middle" font-size="9" font-weight="600" fill="#dc2626" letter-spacing="1">LOCK RELEASE</text>
<text x="260" y="210" text-anchor="middle" font-size="8" fill="#be123c">rm -rf .stratum/locks/changelog</text>
<text x="260" y="222" text-anchor="middle" font-size="8" fill="#94a3b8">triggered by EXIT trap</text>
<!-- outputs → lock release arrow -->
<line x1="356" y1="212" x2="336" y2="212" stroke="#dc2626" stroke-width="1.5" marker-end="url(#ar)"/>
<!-- ─── separator A4 → gate validation ─── -->
<line x1="441" y1="262" x2="441" y2="310" stroke="#059669" stroke-width="1" marker-end="url(#ag)"/>
<!-- A6: Gate/ADR validation -->
<rect x="16" y="312" width="510" height="68" rx="6" fill="#f1f5f9" stroke="#f59e0b" stroke-width="1"/>
<text x="271" y="330" text-anchor="middle" font-size="9" font-weight="600" fill="#92400e" letter-spacing="1">ADR CONSTRAINT VALIDATION (stratum adr validate)</text>
<text x="271" y="346" text-anchor="middle" font-size="8" fill="#b45309">nickel export adrs/*.ncl | where status == Accepted | get constraints | where severity == Hard</text>
<text x="271" y="360" text-anchor="middle" font-size="8" fill="#b45309">for each: nu -c check_hint → exit 0 + no stdout = pass</text>
<text x="271" y="372" text-anchor="middle" font-size="8" fill="#94a3b8">^nickel export (not plugin cache) — always fresh · Superseded ADRs excluded</text>
<!-- ═══════════════════════════════════════════════════════════════════
FLOW B: CONFIG APPLY
═══════════════════════════════════════════════════════════════════ -->
<!-- vertical separator line -->
<line x1="538" y1="56" x2="538" y2="390" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="4,3"/>
<!-- B1: Actor / trigger -->
<rect x="546" y="82" width="148" height="64" rx="6" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="620" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#334155" letter-spacing="1">ACTOR</text>
<text x="620" y="112" text-anchor="middle" font-size="8" fill="#64748b">developer (TTY)</text>
<text x="620" y="124" text-anchor="middle" font-size="8" fill="#64748b">agent (piped)</text>
<text x="620" y="136" text-anchor="middle" font-size="8" fill="#64748b">ci / admin</text>
<line x1="694" y1="114" x2="712" y2="114" stroke="#64748b" stroke-width="1.5" marker-end="url(#af)"/>
<!-- B2: Lock acquire (manifest) -->
<rect x="714" y="82" width="152" height="64" rx="6" fill="#fff5f5" stroke="#ef4444" stroke-width="1"/>
<text x="790" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#dc2626" letter-spacing="1">LOCK ACQUIRE</text>
<text x="790" y="112" text-anchor="middle" font-size="8" fill="#be123c">mkdir .stratum/locks/manifest</text>
<text x="790" y="124" text-anchor="middle" font-size="8" fill="#be123c">write PID to lock dir</text>
<text x="790" y="136" text-anchor="middle" font-size="8" fill="#94a3b8">stale? kill -0 PID → remove</text>
<line x1="866" y1="114" x2="884" y2="114" stroke="#64748b" stroke-width="1.5" marker-end="url(#af)"/>
<!-- B3: nickel export + hash -->
<rect x="886" y="82" width="178" height="64" rx="6" fill="#eff6ff" stroke="#3b82f6" stroke-width="1"/>
<text x="975" y="98" text-anchor="middle" font-size="9" font-weight="600" fill="#1d4ed8" letter-spacing="1">EXPORT + HASH</text>
<text x="975" y="112" text-anchor="middle" font-size="8" fill="#0284c7">nickel export &lt;profile&gt;.ncl</text>
<text x="975" y="124" text-anchor="middle" font-size="8" fill="#0284c7">sha256(export) → seal.hash</text>
<text x="975" y="136" text-anchor="middle" font-size="8" fill="#94a3b8">sha256(snapshot) → snapshot_hash</text>
<line x1="975" y1="146" x2="975" y2="162" stroke="#2563eb" stroke-width="1.5" marker-end="url(#ab)"/>
<!-- B4: write history entry -->
<rect x="886" y="164" width="178" height="80" rx="6" fill="#eff6ff" stroke="#3b82f6" stroke-width="1"/>
<text x="975" y="180" text-anchor="middle" font-size="9" font-weight="600" fill="#1d4ed8" letter-spacing="1">HISTORY ENTRY</text>
<text x="975" y="195" text-anchor="middle" font-size="8" fill="#0284c7">cfg-&lt;YYYYMMDDTHHmmSS&gt;-&lt;actor&gt;.ncl</text>
<text x="975" y="208" text-anchor="middle" font-size="8" fill="#0284c7">ConfigState record (immutable)</text>
<text x="975" y="221" text-anchor="middle" font-size="8" fill="#0284c7">seal.hash + snapshot_hash</text>
<text x="975" y="234" text-anchor="middle" font-size="8" fill="#0284c7">supersedes: prev entry | null</text>
<line x1="975" y1="244" x2="975" y2="260" stroke="#2563eb" stroke-width="1.5" marker-end="url(#ab)"/>
<!-- B5: patch manifest -->
<rect x="886" y="262" width="178" height="60" rx="6" fill="#eff6ff" stroke="#3b82f6" stroke-width="1"/>
<text x="975" y="278" text-anchor="middle" font-size="9" font-weight="600" fill="#1d4ed8" letter-spacing="1">PATCH MANIFEST</text>
<text x="975" y="293" text-anchor="middle" font-size="8" fill="#0284c7">manifest.ncl → active[&lt;profile&gt;]</text>
<text x="975" y="306" text-anchor="middle" font-size="8" fill="#0284c7">= &lt;new cfg entry ref&gt;</text>
<text x="975" y="318" text-anchor="middle" font-size="8" fill="#94a3b8">under manifest lock</text>
<!-- B6: Lock release -->
<rect x="714" y="178" width="152" height="52" rx="6" fill="#fff5f5" stroke="#ef4444" stroke-width="1"/>
<text x="790" y="196" text-anchor="middle" font-size="9" font-weight="600" fill="#dc2626" letter-spacing="1">LOCK RELEASE</text>
<text x="790" y="210" text-anchor="middle" font-size="8" fill="#be123c">rm -rf .stratum/locks/manifest</text>
<text x="790" y="222" text-anchor="middle" font-size="8" fill="#94a3b8">triggered by EXIT trap</text>
<!-- patch manifest → lock release -->
<line x1="886" y1="292" x2="866" y2="292" stroke="#dc2626" stroke-width="1.5" marker-end="url(#ar)"/>
<!-- B7: audit (drift check) sits below -->
<line x1="975" y1="322" x2="975" y2="370" stroke="#2563eb" stroke-width="1" marker-end="url(#ab)"/>
<!-- ─── B vertical: audit box ─── -->
<rect x="546" y="312" width="510" height="68" rx="6" fill="#f1f5f9" stroke="#3b82f6" stroke-width="1"/>
<text x="801" y="330" text-anchor="middle" font-size="9" font-weight="600" fill="#1d4ed8" letter-spacing="1">DRIFT AUDIT (stratum config audit)</text>
<text x="801" y="346" text-anchor="middle" font-size="8" fill="#0284c7">for each profile in manifest: sha256(live profile.ncl) == manifest.active[p].seal.hash</text>
<text x="801" y="360" text-anchor="middle" font-size="8" fill="#0284c7">match → clean · mismatch → drift (warn actor, optionally block)</text>
<text x="801" y="372" text-anchor="middle" font-size="8" fill="#94a3b8">read-only; no lock acquired · rollback: verify snapshot_hash → restore → new apply (forward operation)</text>
<!-- ═══ SUMMARY / EXAMPLES ════════════════════════════════════════════ -->
<rect x="12" y="398" width="1056" height="90" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
<text x="540" y="418" text-anchor="middle" font-size="10" font-weight="700" fill="#64748b">actor flows through both paths automatically — locking is per-resource, not global</text>
<rect x="28" y="428" width="230" height="50" rx="5" fill="#f0fdf4" stroke="#166534" stroke-width="1"/>
<text x="143" y="444" text-anchor="middle" font-size="8" fill="#16a34a">stratum.sh register</text>
<text x="143" y="457" text-anchor="middle" font-size="8" fill="#166534">STRATUM_ACTOR=developer (TTY)</text>
<text x="143" y="470" text-anchor="middle" font-size="8" fill="#94a3b8">typedialog form → CHANGELOG locked</text>
<rect x="272" y="428" width="230" height="50" rx="5" fill="#fffbeb" stroke="#92400e" stroke-width="1"/>
<text x="387" y="444" text-anchor="middle" font-size="8" fill="#b45309">stratum.sh config apply prod</text>
<text x="387" y="457" text-anchor="middle" font-size="8" fill="#92400e">STRATUM_ACTOR=ci</text>
<text x="387" y="470" text-anchor="middle" font-size="8" fill="#94a3b8">manifest lock → cfg-ts-ci.ncl written</text>
<rect x="516" y="428" width="230" height="50" rx="5" fill="#f5f3ff" stroke="#4338ca" stroke-width="1"/>
<text x="631" y="444" text-anchor="middle" font-size="8" fill="#7c3aed">stratum.sh config audit</text>
<text x="631" y="457" text-anchor="middle" font-size="8" fill="#4338ca">any actor, no lock needed</text>
<text x="631" y="470" text-anchor="middle" font-size="8" fill="#94a3b8">sha256 compare live vs seal.hash</text>
<rect x="760" y="428" width="296" height="50" rx="5" fill="#eff6ff" stroke="#1d4ed8" stroke-width="1"/>
<text x="908" y="444" text-anchor="middle" font-size="8" fill="#1d4ed8">stratum.sh config rollback &lt;ts&gt;</text>
<text x="908" y="457" text-anchor="middle" font-size="8" fill="#0284c7">verify snapshot_hash → restore → new apply</text>
<text x="908" y="470" text-anchor="middle" font-size="8" fill="#94a3b8">rollback is always a forward operation</text>
<!-- ═══ LEGEND ═══════════════════════════════════════════════════════ -->
<rect x="12" y="502" width="1056" height="58" rx="8" fill="#f1f5f9" stroke="#cbd5e1" stroke-width="1"/>
<text x="40" y="522" font-size="9" fill="#64748b">Legend:</text>
<line x1="100" y1="518" x2="132" y2="518" stroke="#64748b" stroke-width="1.5" marker-end="url(#af)"/>
<text x="138" y="522" font-size="8" fill="#94a3b8">main flow</text>
<line x1="220" y1="518" x2="252" y2="518" stroke="#059669" stroke-width="1.5" marker-end="url(#ag)"/>
<text x="258" y="522" font-size="8" fill="#94a3b8">pass / ok</text>
<line x1="340" y1="518" x2="372" y2="518" stroke="#dc2626" stroke-width="1.5" marker-end="url(#ar)"/>
<text x="378" y="522" font-size="8" fill="#94a3b8">error / lock release</text>
<line x1="478" y1="518" x2="510" y2="518" stroke="#d97706" stroke-width="1.5" marker-end="url(#aa)"/>
<text x="516" y="522" font-size="8" fill="#94a3b8">form complete</text>
<line x1="604" y1="518" x2="636" y2="518" stroke="#2563eb" stroke-width="1.5" marker-end="url(#ab)"/>
<text x="642" y="522" font-size="8" fill="#94a3b8">config flow (hash/write)</text>
<line x1="748" y1="518" x2="780" y2="518" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#av)"/>
<text x="786" y="522" font-size="8" fill="#94a3b8">output / audit</text>
<text x="40" y="542" font-size="8" fill="#64748b">Lock resources:</text>
<text x="130" y="542" font-size="8" fill="#94a3b8">changelog (register) · manifest (config apply/rollback) · backlog (backlog done/cancel)</text>
<text x="40" y="555" font-size="8" fill="#64748b">History IDs:</text>
<text x="108" y="555" font-size="8" fill="#94a3b8">cfg-&lt;YYYYMMDDTHHmmSS&gt;-&lt;actor&gt;.ncl — timestamp collision-free, no lock needed for history writes</text>
<!-- ═══ BACKLOG SUB-FLOW ═══════════════════════════════════════════ -->
<rect x="12" y="574" width="1056" height="90" rx="8" fill="#f1f5f9" stroke="#f59e0b" stroke-width="1"/>
<text x="36" y="592" font-size="9" font-weight="600" fill="#d97706" letter-spacing="1">FLOW C — BACKLOG (stratum backlog …)</text>
<rect x="28" y="600" width="170" height="54" rx="5" fill="#fafaf9" stroke="#f59e0b" stroke-width="1"/>
<text x="113" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#92400e">backlog list|show</text>
<text x="113" y="629" text-anchor="middle" font-size="8" fill="#b45309">read backlog.ncl (no lock)</text>
<text x="113" y="642" text-anchor="middle" font-size="8" fill="#94a3b8">nickel export → filter/sort</text>
<line x1="198" y1="627" x2="216" y2="627" stroke="#d97706" stroke-width="1.5" marker-end="url(#aa)"/>
<rect x="218" y="600" width="178" height="54" rx="5" fill="#fafaf9" stroke="#f59e0b" stroke-width="1"/>
<text x="307" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#92400e">backlog add</text>
<text x="307" y="629" text-anchor="middle" font-size="8" fill="#b45309">no lock (append, unique ID)</text>
<text x="307" y="642" text-anchor="middle" font-size="8" fill="#94a3b8">typed item: Todo|Bug|Wish|Debt</text>
<line x1="396" y1="627" x2="414" y2="627" stroke="#d97706" stroke-width="1.5" marker-end="url(#aa)"/>
<rect x="416" y="600" width="192" height="54" rx="5" fill="#fafaf9" stroke="#ef4444" stroke-width="1"/>
<text x="512" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#dc2626">backlog done|cancel</text>
<text x="512" y="629" text-anchor="middle" font-size="8" fill="#be123c">mkdir lock on backlog resource</text>
<text x="512" y="642" text-anchor="middle" font-size="8" fill="#94a3b8">mutates status field in backlog.ncl</text>
<line x1="608" y1="627" x2="626" y2="627" stroke="#d97706" stroke-width="1.5" marker-end="url(#aa)"/>
<rect x="628" y="600" width="190" height="54" rx="5" fill="#fafaf9" stroke="#f59e0b" stroke-width="1"/>
<text x="723" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#92400e">backlog promote</text>
<text x="723" y="629" text-anchor="middle" font-size="8" fill="#b45309">graduates_to: Adr|Mode|StateTransition</text>
<text x="723" y="642" text-anchor="middle" font-size="8" fill="#94a3b8">triggers adr or mode stub creation</text>
<line x1="818" y1="627" x2="836" y2="627" stroke="#d97706" stroke-width="1.5" marker-end="url(#aa)"/>
<rect x="838" y="600" width="216" height="54" rx="5" fill="#fafaf9" stroke="#f59e0b" stroke-width="1"/>
<text x="946" y="616" text-anchor="middle" font-size="8" font-weight="600" fill="#92400e">backlog roadmap</text>
<text x="946" y="629" text-anchor="middle" font-size="8" fill="#b45309">state.ncl dimensions not-yet-reached</text>
<text x="946" y="642" text-anchor="middle" font-size="8" fill="#94a3b8">open items by priority (read-only)</text>
<text x="1068" y="770" text-anchor="end" font-size="8" fill="#cbd5e1">2026-03</text>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,477 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1170 830">
<defs>
<style>
.flow { animation: flow-a 3s linear infinite; }
.flow-fast { animation: flow-a 1.8s linear infinite; }
.flow-slow { animation: flow-a 5s linear infinite; }
.flow-vslow { animation: flow-a 7s linear infinite; }
@keyframes flow-a { from { stroke-dashoffset: 24; } to { stroke-dashoffset: 0; } }
.orbit { animation: orbit-a 8s linear infinite; }
@keyframes orbit-a { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -68; } }
.glow-pulse { animation: glow-a 3s ease-in-out infinite; }
@keyframes glow-a { 0%,100% { opacity:.08; } 50% { opacity:.28; } }
.hb { animation: hb-a 3s ease-in-out infinite; }
.hb-d1 { animation-delay: .4s; }
.hb-d2 { animation-delay: .8s; }
.hb-d3 { animation-delay:1.2s; }
.hb-d4 { animation-delay:1.6s; }
.hb-d5 { animation-delay:2.0s; }
@keyframes hb-a { 0%,100% { opacity:.85; } 50% { opacity:1; } }
.particle-spin1 { animation: spin1 6s linear infinite; }
.particle-spin2 { animation: spin2 9s linear infinite; }
.particle-spin3 { animation: spin3 14s linear infinite; }
@keyframes spin1 { from { transform: rotate(0deg) translateX(120px); } to { transform: rotate(360deg) translateX(120px); } }
@keyframes spin2 { from { transform: rotate(120deg) translateX(120px); } to { transform: rotate(480deg) translateX(120px); } }
@keyframes spin3 { from { transform: rotate(240deg) translateX(120px); } to { transform: rotate(600deg) translateX(120px); } }
</style>
<radialGradient id="bg-grad" cx="50%" cy="42%" r="58%">
<stop offset="0%" stop-color="#131929"/>
<stop offset="100%" stop-color="#0F172A"/>
</radialGradient>
<linearGradient id="title-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#1e1b4b"/>
<stop offset="50%" stop-color="#0f172a"/>
<stop offset="100%" stop-color="#1e1b4b"/>
</linearGradient>
<linearGradient id="orch-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e1b4b"/>
<stop offset="100%" stop-color="#0d0b22"/>
</linearGradient>
<linearGradient id="auth-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1c1700"/>
<stop offset="100%" stop-color="#0f0e00"/>
</linearGradient>
<linearGradient id="surreal-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1a0f2e"/>
<stop offset="100%" stop-color="#100820"/>
</linearGradient>
<linearGradient id="vault-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1c1400"/>
<stop offset="100%" stop-color="#110d00"/>
</linearGradient>
<linearGradient id="oci-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#021f20"/>
<stop offset="100%" stop-color="#011314"/>
</linearGradient>
<linearGradient id="forgejo-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e0f02"/>
<stop offset="100%" stop-color="#120900"/>
</linearGradient>
<linearGradient id="exec-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#021a0b"/>
<stop offset="100%" stop-color="#011007"/>
</linearGradient>
<linearGradient id="agent-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#12082a"/>
<stop offset="100%" stop-color="#0a0518"/>
</linearGradient>
<linearGradient id="ncl-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#6366F1" stop-opacity=".25"/>
<stop offset="50%" stop-color="#22D3EE" stop-opacity=".15"/>
<stop offset="100%" stop-color="#6366F1" stop-opacity=".25"/>
</linearGradient>
<linearGradient id="event-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e0a00"/>
<stop offset="100%" stop-color="#110600"/>
</linearGradient>
<linearGradient id="mod-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1a1850"/>
<stop offset="100%" stop-color="#10102e"/>
</linearGradient>
<linearGradient id="mod-ag-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1E1060"/>
<stop offset="100%" stop-color="#140C48"/>
</linearGradient>
<linearGradient id="mod-pc-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#082030"/>
<stop offset="100%" stop-color="#041520"/>
</linearGradient>
<linearGradient id="mod-sr-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0A2010"/>
<stop offset="100%" stop-color="#061508"/>
</linearGradient>
<linearGradient id="mod-re-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#201800"/>
<stop offset="100%" stop-color="#140F00"/>
</linearGradient>
<linearGradient id="orch-stripe" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#6366F1"/>
<stop offset="100%" stop-color="#22D3EE"/>
</linearGradient>
<filter id="shadow" x="-15%" y="-15%" width="130%" height="130%">
<feDropShadow dx="0" dy="3" stdDeviation="8" flood-color="#000" flood-opacity=".5"/>
</filter>
<filter id="glow-c" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<marker id="arr-cyan" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#22D3EE"/></marker>
<marker id="arr-gold" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#F59E0B"/></marker>
<marker id="arr-green" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#10B981"/></marker>
<marker id="arr-orange" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#F97316"/></marker>
<marker id="arr-purple" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#A78BFA"/></marker>
<marker id="arr-indigo" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#818CF8"/></marker>
<marker id="arr-silver" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#64748B"/></marker>
<marker id="arr-dcyan" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#06B6D4"/></marker>
<!-- ─── stratumiops-h logo defs ─── -->
<linearGradient id="hpLayerGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366F1"/><stop offset="100%" style="stop-color:#4F46E5"/>
</linearGradient>
<linearGradient id="hpProcessorGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#22D3EE"/><stop offset="100%" style="stop-color:#06B6D4"/>
</linearGradient>
<linearGradient id="hpFlowGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#6366F1"/><stop offset="50%" style="stop-color:#22D3EE"/><stop offset="100%" style="stop-color:#6366F1"/>
<animate attributeName="x1" values="0%;100%;0%" dur="2.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="100%;200%;100%" dur="2.5s" repeatCount="indefinite"/>
</linearGradient>
<linearGradient id="hpUnderlineGrad" x1="280" y1="195" x2="750" y2="195" gradientUnits="userSpaceOnUse">
<stop offset="0%" style="stop-color:#6366F1"/><stop offset="50%" style="stop-color:#22D3EE"/><stop offset="100%" style="stop-color:#6366F1"/>
</linearGradient>
<linearGradient id="hpShimmer" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#6366F1"/><stop offset="40%" style="stop-color:#6366F1"/>
<stop offset="50%" style="stop-color:#22D3EE"/><stop offset="60%" style="stop-color:#6366F1"/><stop offset="100%" style="stop-color:#6366F1"/>
<animate attributeName="x1" values="-100%;100%" dur="3s" repeatCount="indefinite" begin="2s"/>
<animate attributeName="x2" values="0%;200%" dur="3s" repeatCount="indefinite" begin="2s"/>
</linearGradient>
<filter id="hpProcessorGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" result="coloredBlur"><animate attributeName="stdDeviation" values="4;8;4" dur="1.5s" repeatCount="indefinite"/></feGaussianBlur>
<feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="hpNodeGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<clipPath id="hpTextReveal">
<rect x="280" y="80" width="0" height="150">
<animate attributeName="width" values="0;800" dur="1s" fill="freeze" begin="1s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/>
</rect>
</clipPath>
<path id="hpPathIn1" d="M25 40 Q25 65 55 80 Q85 95 95 100" fill="none"/>
<path id="hpPathIn2" d="M215 40 Q215 65 185 80 Q155 95 145 100" fill="none"/>
<path id="hpPathOut1" d="M95 100 Q85 105 55 120 Q25 135 25 160" fill="none"/>
<path id="hpPathOut2" d="M145 100 Q155 105 185 120 Q215 135 215 160" fill="none"/>
</defs>
<!-- ─── BACKGROUND ─── -->
<rect width="1170" height="830" fill="url(#bg-grad)"/>
<g opacity=".025">
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M40 0L0 0 0 40" fill="none" stroke="#fff" stroke-width=".5"/>
</pattern>
<rect width="1170" height="830" fill="url(#grid)"/>
</g>
<g opacity=".3">
<circle cx="90" cy="200" r=".8" fill="#fff"/>
<circle cx="420" cy="88" r=".6" fill="#fff"/>
<circle cx="980" cy="130" r="1" fill="#fff"/>
<circle cx="55" cy="600" r=".5" fill="#fff"/>
<circle cx="700" cy="800" r=".6" fill="#fff"/>
</g>
<!-- ─── TITLE BAR ─── -->
<rect x="0" y="0" width="1170" height="60" fill="url(#title-grad)"/>
<line x1="0" y1="60" x2="1170" y2="60" stroke="#6366F1" stroke-width=".8" opacity=".5"/>
<text x="600" y="30" text-anchor="middle" fill="#22D3EE" font-family="'IBM Plex Mono',monospace" font-size="19" font-weight="700" letter-spacing="3" dominant-baseline="middle">STRATUM ORCHESTRATOR</text>
<text x="600" y="50" text-anchor="middle" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="9" letter-spacing="1.5">Event-Driven · Graph-Guided · Atomic Execution · Stateless · OCI-Native</text>
<!-- ─── EVENT SOURCES ROW ─── -->
<text x="400" y="82" text-anchor="middle" fill="#F97316" font-family="'IBM Plex Mono',monospace" font-size="8" letter-spacing="2" opacity=".8">EVENT SOURCES</text>
<rect x="55" y="90" width="128" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb"/>
<rect x="55" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="55" y="90" width="128" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".4"/>
<text x="70" y="111" fill="#fff" font-family="Inter,sans-serif" font-size="11" font-weight="600">provisioning</text>
<text x="70" y="127" fill="#F97316" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".8">emits dev.crate.&gt;</text>
<text x="70" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">crate-modified · deploy</text>
<rect x="200" y="90" width="118" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb hb-d1"/>
<rect x="200" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="200" y="90" width="118" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".4"/>
<text x="215" y="111" fill="#fff" font-family="Inter,sans-serif" font-size="11" font-weight="600">kogral</text>
<text x="215" y="127" fill="#F97316" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".8">emits dev.knowledge.&gt;</text>
<text x="215" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">node-updated · indexed</text>
<rect x="335" y="90" width="120" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb hb-d2"/>
<rect x="335" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="335" y="90" width="120" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".4"/>
<text x="350" y="111" fill="#fff" font-family="Inter,sans-serif" font-size="11" font-weight="600">syntaxis</text>
<text x="350" y="127" fill="#F97316" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".8">emits dev.project.&gt;</text>
<text x="350" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">phase · task-completed</text>
<rect x="472" y="90" width="135" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb hb-d3"/>
<rect x="472" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="472" y="90" width="135" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".4"/>
<text x="487" y="111" fill="#fff" font-family="Inter,sans-serif" font-size="11" font-weight="600">stratumiops</text>
<text x="487" y="127" fill="#F97316" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".8">emits dev.model.&gt;</text>
<text x="487" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">llm-call · embed-request</text>
<rect x="624" y="90" width="118" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb hb-d4"/>
<rect x="624" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="624" y="90" width="118" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".4"/>
<text x="639" y="111" fill="#fff" font-family="Inter,sans-serif" font-size="11" font-weight="600">typedialog</text>
<text x="639" y="127" fill="#F97316" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".8">emits dev.form.&gt;</text>
<text x="639" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">submitted · validated</text>
<rect x="755" y="90" width="120" height="60" rx="6" fill="none" stroke="#64748B" stroke-width=".8" stroke-dasharray="4 3" opacity=".5"/>
<text x="815" y="127" text-anchor="middle" fill="#64748B" font-family="Inter,sans-serif" font-size="19">+ more ...</text>
<!-- ─── CONNECTIONS: PROJECTS → NATS (NATS cx=355 cy=385 r=129 → anillo exterior que pulsa) ─── -->
<line x1="119" y1="150" x2="317" y2="262" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<line x1="259" y1="150" x2="337" y2="257" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<line x1="395" y1="150" x2="355" y2="256" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<line x1="539" y1="150" x2="374" y2="258" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<line x1="686" y1="150" x2="396" y2="263" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<!-- AUTH LAYER removed: NKeys → badge on NATS→Orch arrow; Cedar → inside RuleEngine -->
<!-- ─── NATS ORBITAL RING (cx=355, cy=385, r×0.9: 129/123/120/66) ─── -->
<circle cx="355" cy="385" r="129" fill="none" stroke="#22D3EE" stroke-width="14" opacity=".04" class="glow-pulse"/>
<circle cx="355" cy="385" r="123" fill="none" stroke="#22D3EE" stroke-width="10" opacity=".06" class="glow-pulse" filter="url(#glow-c)"/>
<circle cx="355" cy="385" r="120" fill="none" stroke="#64748B" stroke-width="1.5" stroke-dasharray="10 7" class="orbit"/>
<circle cx="355" cy="385" r="66" fill="none" stroke="#6366F1" stroke-width=".5" opacity=".2"/>
<radialGradient id="nats-inner" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#22D3EE" stop-opacity=".12"/>
<stop offset="100%" stop-color="#6366F1" stop-opacity=".04"/>
</radialGradient>
<circle cx="355" cy="385" r="66" fill="url(#nats-inner)"/>
<text x="355" y="379" text-anchor="middle" fill="#22D3EE" font-family="'IBM Plex Mono',monospace" font-size="20" font-weight="700" filter="url(#glow-c)">NATS</text>
<text x="355" y="398" text-anchor="middle" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="9">JetStream</text>
<text x="344" y="235" text-anchor="middle" fill="#22D3EE" font-family="'IBM Plex Mono',monospace" font-size="9" opacity=".7">dev.&gt;</text>
<g transform="translate(355,385)">
<circle r="4" fill="#22D3EE" opacity=".9" class="particle-spin1"/>
<circle r="3.5" fill="#6366F1" opacity=".7" class="particle-spin2"/>
<circle r="3" fill="#F97316" opacity=".5" class="particle-spin3"/>
</g>
<!-- ─── NATS → ORCHESTRATOR (orbit right x=475, orch left x=498) — NKey checkpoint badge ─── -->
<path d="M 475,385 L 495,385" stroke="#22D3EE" stroke-width="3" stroke-dasharray="8 4" class="flow-fast" marker-end="url(#arr-cyan)" filter="url(#glow-c)"/>
<circle r="4" fill="#22D3EE" opacity=".85">
<animateMotion dur="0.25s" repeatCount="indefinite" path="M 475,385 L 495,385"/>
</circle>
<circle r="3" fill="#6366F1" opacity=".7">
<animateMotion dur="0.25s" repeatCount="indefinite" begin="0.08s" path="M 475,385 L 495,385"/>
</circle>
<!-- NKey verify badge — checkpoint at connection boundary -->
<rect x="379" y="419" width="78" height="22" rx="3" fill="#1c1700" stroke="#F59E0B" stroke-width=".7" opacity=".9"/>
<text x="418" y="429" text-anchor="middle" fill="#F59E0B" font-family="'IBM Plex Mono',monospace" font-size="7">NKey verify</text>
<text x="418" y="439" text-anchor="middle" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="6.5">ed25519 JWT</text>
<!-- ─── ORCHESTRATOR BOX (x=498, y=250, w=375, h=300, center x=685) ─── -->
<rect x="498" y="250" width="375" height="300" rx="10" fill="#000" opacity=".2" transform="translate(3,5)"/>
<rect x="498" y="250" width="375" height="300" rx="10" fill="url(#orch-grad)" filter="url(#shadow)" class="hb"/>
<rect x="498" y="250" width="375" height="300" rx="10" fill="none" stroke="#6366F1" stroke-width=".8" opacity=".5" class="glow-pulse"/>
<rect x="498" y="250" width="4" height="300" rx="2" fill="url(#orch-stripe)"/>
<text x="685" y="273" text-anchor="middle" fill="#fff" font-family="Inter,sans-serif" font-size="14" font-weight="700">STRATUM ORCHESTRATOR</text>
<text x="685" y="290" text-anchor="middle" fill="#64748B" font-family="Inter,sans-serif" font-size="9">Agnostic · Stateless · Graph-Guided</text>
<!-- Internal modules 2×2 (cols of 170px each, gap 8px) -->
<!-- ActionGraph -->
<rect x="510" y="305" width="170" height="72" rx="6" fill="url(#mod-ag-grad)" stroke="#4F46E5" stroke-width=".7" opacity=".9"/>
<text x="522" y="325" fill="#A5B4FC" font-family="Inter,sans-serif" font-size="11" font-weight="600">◈ ActionGraph</text>
<text x="522" y="341" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">in-memory · Nickel nodes</text>
<text x="522" y="355" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">topo-sort · cycle-detect</text>
<!-- PipelineContext -->
<rect x="690" y="305" width="171" height="72" rx="6" fill="url(#mod-pc-grad)" stroke="#06B6D4" stroke-width=".7" opacity=".9"/>
<text x="702" y="325" fill="#A5B4FC" font-family="Inter,sans-serif" font-size="11" font-weight="600">⬡ PipelineCtx</text>
<text x="702" y="341" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">DB-first · typed caps</text>
<text x="702" y="355" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">schema-validated</text>
<!-- StageRunner -->
<rect x="510" y="389" width="170" height="130" rx="6" fill="url(#mod-sr-grad)" stroke="#10B981" stroke-width=".7" opacity=".9"/>
<text x="522" y="407" fill="#A5B4FC" font-family="Inter,sans-serif" font-size="11" font-weight="600">▶ StageRunner</text>
<text x="522" y="423" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">JoinSet · parallel stages</text>
<text x="522" y="437" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">CancellationToken</text>
<text x="522" y="451" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">retry + backoff on failure</text>
<text x="522" y="465" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">saga compensate.nu</text>
<!-- RuleEngine (Cedar + NKey as named sub-sections) -->
<rect x="690" y="389" width="171" height="130" rx="6" fill="url(#mod-re-grad)" stroke="#F59E0B" stroke-width=".7" opacity=".9"/>
<text x="702" y="407" fill="#A5B4FC" font-family="Inter,sans-serif" font-size="11" font-weight="600">⚙ RuleEngine</text>
<rect x="810" y="393" width="44" height="13" rx="3" fill="#1c1700" stroke="#F59E0B" stroke-width=".6"/>
<text x="832" y="402" text-anchor="middle" fill="#F59E0B" font-family="'IBM Plex Mono',monospace" font-size="7">Cedar</text>
<line x1="702" y1="415" x2="854" y2="415" stroke="#4F46E5" stroke-width=".4" opacity=".5"/>
<text x="702" y="428" fill="#F59E0B" font-family="'IBM Plex Mono',monospace" font-size="8" font-weight="600">◈ Cedar</text>
<text x="702" y="441" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">permit · forbid · conditions</text>
<text x="702" y="454" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">per-node authz policies</text>
<line x1="702" y1="463" x2="854" y2="463" stroke="#4F46E5" stroke-width=".4" opacity=".5"/>
<text x="702" y="476" fill="#F59E0B" font-family="'IBM Plex Mono',monospace" font-size="8" font-weight="600">◈ NKey</text>
<text x="702" y="489" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">ed25519 asymmetric keys</text>
<text x="702" y="502" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">JWT per-process · verify</text>
<text x="685" y="538" text-anchor="middle" fill="#F87171" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".75">↺ Saga rollback on failure · compensate.nu in reverse</text>
<!-- ─── RIGHT PANEL: DATA STORES (x=926, w=232, ends x=1158) ─── -->
<!-- Orch right x=873 → stores: flechas a sus centros verticales (cajas 25px arriba) -->
<path d="M 873,256 L 926,216" stroke="#A78BFA" stroke-width="1.5" stroke-dasharray="4 3" class="flow" marker-end="url(#arr-purple)" fill="none" opacity=".8"/>
<path d="M 873,350 L 926,310" stroke="#F59E0B" stroke-width="1.5" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-gold)" fill="none" opacity=".8"/>
<path d="M 873,444 L 926,404" stroke="#06B6D4" stroke-width="1" stroke-dasharray="3 4" class="flow-slow" marker-end="url(#arr-dcyan)" fill="none" opacity=".7"/>
<!-- SurrealDB -->
<rect x="926" y="175" width="197" height="82" rx="8" fill="url(#surreal-grad)" filter="url(#shadow)" class="hb hb-d2"/>
<rect x="926" y="175" width="3" height="82" rx="1" fill="#A78BFA"/>
<rect x="926" y="175" width="197" height="82" rx="8" fill="none" stroke="#A78BFA" stroke-width=".8" opacity=".5"/>
<text x="941" y="198" fill="#A78BFA" font-family="Inter,sans-serif" font-size="12" font-weight="700">SurrealDB</text>
<text x="941" y="215" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">Pipeline state · Step results</text>
<text x="941" y="231" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">orchestrator_state ns</text>
<text x="941" y="247" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">crash recovery</text>
<!-- SecretumVault -->
<rect x="926" y="269" width="197" height="82" rx="8" fill="url(#vault-grad)" filter="url(#shadow)" class="hb hb-d3"/>
<rect x="926" y="269" width="3" height="82" rx="1" fill="#F59E0B"/>
<rect x="926" y="269" width="197" height="82" rx="8" fill="none" stroke="#F59E0B" stroke-width=".8" opacity=".5"/>
<text x="941" y="292" fill="#F59E0B" font-family="Inter,sans-serif" font-size="12" font-weight="700">SecretumVault</text>
<text x="941" y="309" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">Credentials · TTL leases</text>
<text x="941" y="325" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">vault:/secret/...</text>
<text x="941" y="341" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">never in NATS payload</text>
<!-- Zot OCI -->
<rect x="926" y="363" width="197" height="82" rx="8" fill="url(#oci-grad)" filter="url(#shadow)" class="hb hb-d4"/>
<rect x="926" y="363" width="3" height="82" rx="1" fill="#06B6D4"/>
<rect x="926" y="363" width="197" height="82" rx="8" fill="none" stroke="#06B6D4" stroke-width=".8" opacity=".5"/>
<text x="941" y="386" fill="#06B6D4" font-family="Inter,sans-serif" font-size="12" font-weight="700">Zot OCI Registry</text>
<text x="941" y="403" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">Node defs · Nickel libs</text>
<text x="941" y="419" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">oci://registry/nodes/</text>
<text x="941" y="435" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">content-addressed · signed</text>
<!-- ─── RIGHT PANEL: OPTIONAL SERVICES ─── -->
<text x="1042" y="533" text-anchor="middle" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8" letter-spacing="2">OPTIONAL</text>
<!-- Flechas opcionales: escalonadas, cajas 40px más abajo -->
<path d="M 873,495 C 903,495 903,559 926,559" stroke="#64748B" stroke-width="1" stroke-dasharray="3 5" class="flow-vslow" marker-end="url(#arr-silver)" fill="none" opacity=".5"/>
<path d="M 873,510 C 905,510 905,653 926,653" stroke="#64748B" stroke-width="1" stroke-dasharray="3 5" class="flow-vslow" marker-end="url(#arr-silver)" fill="none" opacity=".5"/>
<path d="M 873,525 C 907,525 907,744 926,744" stroke="#64748B" stroke-width="1" stroke-dasharray="3 5" class="flow-vslow" marker-end="url(#arr-silver)" fill="none" opacity=".4"/>
<!-- Kogral -->
<rect x="926" y="518" width="197" height="82" rx="8" fill="#0F172A" stroke="#64748B" stroke-width=".8" stroke-dasharray="5 4" opacity=".7" class="hb hb-d2"/>
<text x="941" y="541" fill="#94A3B8" font-family="Inter,sans-serif" font-size="12" font-weight="600">Kogral</text>
<text x="941" y="558" fill="#64748B" font-family="Inter,sans-serif" font-size="9">Knowledge graph</text>
<text x="941" y="574" fill="#4B5563" font-family="'IBM Plex Mono',monospace" font-size="8">node-updated triggers</text>
<rect x="1071" y="522" width="46" height="16" rx="3" fill="#1E293B"/>
<text x="1094" y="533" text-anchor="middle" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">optional</text>
<!-- Syntaxis -->
<rect x="926" y="612" width="197" height="82" rx="8" fill="#0F172A" stroke="#64748B" stroke-width=".8" stroke-dasharray="5 4" opacity=".7" class="hb hb-d3"/>
<text x="941" y="635" fill="#94A3B8" font-family="Inter,sans-serif" font-size="12" font-weight="600">Syntaxis</text>
<text x="941" y="652" fill="#64748B" font-family="Inter,sans-serif" font-size="9">Project orchestration</text>
<text x="941" y="668" fill="#4B5563" font-family="'IBM Plex Mono',monospace" font-size="8">phase-transition events</text>
<rect x="1071" y="616" width="46" height="16" rx="3" fill="#1E293B"/>
<text x="1094" y="627" text-anchor="middle" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">optional</text>
<!-- TypeDialog -->
<rect x="926" y="706" width="197" height="75" rx="8" fill="#0F172A" stroke="#64748B" stroke-width=".8" stroke-dasharray="5 4" opacity=".6" class="hb hb-d4"/>
<text x="941" y="729" fill="#94A3B8" font-family="Inter,sans-serif" font-size="12" font-weight="600">TypeDialog</text>
<text x="941" y="746" fill="#64748B" font-family="Inter,sans-serif" font-size="9">Service config UI</text>
<text x="941" y="764" fill="#4B5563" font-family="'IBM Plex Mono',monospace" font-size="8">startup config NCL only</text>
<!-- ─── LEFT: Git repo (x=10, y=295 — arriba izq junto a NATS) ─── -->
<!-- Orch → Git repo: bezier más curvada para alejarse de NATS -->
<path d="M 498,545 C 120,650 50,350 180,337" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" fill="none" opacity=".5"/>
<!-- Git repo → NATS: línea recta acortada 5px -->
<path d="M 180,337 L 230,353" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" fill="none" opacity=".6"/>
<rect x="30" y="295" width="150" height="85" rx="8" fill="url(#forgejo-grad)" filter="url(#shadow)" class="hb hb-d5"/>
<rect x="30" y="295" width="3" height="85" rx="1" fill="#F97316"/>
<rect x="30" y="295" width="150" height="85" rx="8" fill="none" stroke="#F97316" stroke-width=".8" opacity=".5"/>
<text x="45" y="318" fill="#F97316" font-family="Inter,sans-serif" font-size="12" font-weight="700">Git repo</text>
<text x="45" y="335" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">Git events → NATS</text>
<text x="45" y="351" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">webhook → dev.crate.&gt;</text>
<text x="45" y="367" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">push · tag · pr</text>
<!-- ─── EXECUTION LAYER mismo nivel (Nu x=380 y=623, AI x=615 y=623, sep=35px) ─── -->
<!-- Orch → Nu Executor: sale borde inferior x=510 (20px dcha), U-curve por debajo, llega al fondo (480,733) apuntando arriba, acortada 30px -->
<path d="M 518,550 C 500,560 485,610 483,614" stroke="#10B981" stroke-width="1.5" stroke-dasharray="5 3" class="flow" marker-end="url(#arr-green)" fill="none" opacity=".8"/>
<!-- Orch → AI Agent: copia de Nu pero adaptada a AI, color indigo -->
<path d="M 773,550 C 755,560 717,610 714,616" stroke="#818CF8" stroke-width="1.5" stroke-dasharray="5 3" class="flow" marker-end="url(#arr-indigo)" fill="none" opacity=".8"/>
<rect x="380" y="623" width="200" height="80" rx="8" fill="url(#exec-grad)" filter="url(#shadow)" class="hb"/>
<rect x="380" y="623" width="3" height="80" rx="1" fill="#10B981"/>
<rect x="380" y="623" width="200" height="80" rx="8" fill="none" stroke="#10B981" stroke-width=".8" opacity=".5"/>
<text x="395" y="644" fill="#10B981" font-family="Inter,sans-serif" font-size="12" font-weight="700">Nu Executor</text>
<text x="395" y="660" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">Atomic steps · Pure functions</text>
<text x="395" y="675" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">scripts/nu/*.nu</text>
<text x="395" y="689" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">stdout=output · exit-code=status</text>
<rect x="615" y="623" width="200" height="80" rx="8" fill="url(#agent-grad)" filter="url(#shadow)" class="hb hb-d1"/>
<rect x="615" y="623" width="3" height="80" rx="1" fill="#818CF8"/>
<rect x="615" y="623" width="200" height="80" rx="8" fill="none" stroke="#818CF8" stroke-width=".8" opacity=".5"/>
<text x="630" y="644" fill="#818CF8" font-family="Inter,sans-serif" font-size="12" font-weight="700">AI Agent</text>
<text x="630" y="660" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">stratum-llm · NATS protocol</text>
<text x="630" y="675" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">dev.agent.*.requested/responded</text>
<text x="630" y="689" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">oneshot correlation · timeout</text>
<!-- ─── NICKEL BASE LIBRARY (x=390, y=738) ─── -->
<rect x="390" y="738" width="415" height="65" rx="8" fill="#0F172A" stroke="none"/>
<rect x="390" y="738" width="415" height="65" rx="8" fill="url(#ncl-grad)"/>
<rect x="390" y="738" width="415" height="65" rx="8" fill="none" stroke="#6366F1" stroke-width=".8" stroke-dasharray="6 4" opacity=".6"/>
<text x="597" y="761" text-anchor="middle" fill="#A5B4FC" font-family="Inter,sans-serif" font-size="12" font-weight="600">Nickel Base Library</text>
<text x="597" y="777" text-anchor="middle" fill="#64748B" font-family="Inter,sans-serif" font-size="9">OCI-published · content-addressed · build-verified · typecheck-gated</text>
<text x="597" y="791" text-anchor="middle" fill="#4B5563" font-family="'IBM Plex Mono',monospace" font-size="8">orchestrator-types.ncl · capability-schemas.ncl · defaults.ncl</text>
<!-- Zot OCI → Nickel: arco hacia adentro, sale desde Zot (cajas 40px abajo) -->
<path d="M 926,414 C 900,390 900,750 805,770" stroke="#06B6D4" stroke-width="1" stroke-dasharray="3 3" class="flow-slow" marker-end="url(#arr-dcyan)" fill="none" opacity=".6"/>
<!-- ─── LOGO above LEGEND ─── -->
<g transform="translate(8,5) scale(0.2)">
<g transform="translate(20,50)">
<rect x="-220" y="18" width="220" height="24" rx="5" fill="url(#hpLayerGrad)" opacity="0.7">
<animate attributeName="x" values="-220;0" dur="0.5s" fill="freeze" calcMode="spline" keySplines="0.25 0.1 0.25 1"/>
<animate attributeName="opacity" values="0.5;0.8;0.5" dur="3s" repeatCount="indefinite" begin="1.5s"/>
</rect>
<rect x="-220" y="88" width="220" height="24" rx="5" fill="url(#hpLayerGrad)">
<animate attributeName="x" values="-220;0" dur="0.5s" fill="freeze" begin="0.15s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/>
</rect>
<rect x="-220" y="158" width="220" height="24" rx="5" fill="url(#hpLayerGrad)" opacity="0.7">
<animate attributeName="x" values="-220;0" dur="0.5s" fill="freeze" begin="0.3s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/>
<animate attributeName="opacity" values="0.8;0.5;0.8" dur="3s" repeatCount="indefinite" begin="1.5s"/>
</rect>
<path d="M35 42 Q35 65 70 85 Q95 100 95 100" stroke="url(#hpFlowGrad)" stroke-width="3" fill="none" opacity="0" stroke-linecap="round"><animate attributeName="opacity" values="0;0.6" dur="0.3s" fill="freeze" begin="0.5s"/></path>
<path d="M185 42 Q185 65 150 85 Q125 100 125 100" stroke="url(#hpFlowGrad)" stroke-width="3" fill="none" opacity="0" stroke-linecap="round"><animate attributeName="opacity" values="0;0.6" dur="0.3s" fill="freeze" begin="0.6s"/></path>
<path d="M95 100 Q95 100 70 115 Q35 135 35 158" stroke="url(#hpFlowGrad)" stroke-width="3" fill="none" opacity="0" stroke-linecap="round"><animate attributeName="opacity" values="0;0.6" dur="0.3s" fill="freeze" begin="0.7s"/></path>
<path d="M125 100 Q125 100 150 115 Q185 135 185 158" stroke="url(#hpFlowGrad)" stroke-width="3" fill="none" opacity="0" stroke-linecap="round"><animate attributeName="opacity" values="0;0.6" dur="0.3s" fill="freeze" begin="0.8s"/></path>
<circle r="4" fill="#22D3EE" filter="url(#hpNodeGlow)"><animateMotion dur="1.2s" repeatCount="indefinite" begin="1.5s"><mpath href="#hpPathIn1"/></animateMotion><animate attributeName="opacity" values="1;0.5;0.2" dur="1.2s" repeatCount="indefinite" begin="1.5s"/></circle>
<circle r="4" fill="#22D3EE" filter="url(#hpNodeGlow)"><animateMotion dur="1.2s" repeatCount="indefinite" begin="1.8s"><mpath href="#hpPathIn2"/></animateMotion><animate attributeName="opacity" values="1;0.5;0.2" dur="1.2s" repeatCount="indefinite" begin="1.8s"/></circle>
<circle r="3" fill="#6366F1"><animateMotion dur="1.2s" repeatCount="indefinite" begin="2.1s"><mpath href="#hpPathOut1"/></animateMotion><animate attributeName="opacity" values="0.2;0.5;1" dur="1.2s" repeatCount="indefinite" begin="2.1s"/></circle>
<circle r="3" fill="#6366F1"><animateMotion dur="1.2s" repeatCount="indefinite" begin="2.4s"><mpath href="#hpPathOut2"/></animateMotion><animate attributeName="opacity" values="0.2;0.5;1" dur="1.2s" repeatCount="indefinite" begin="2.4s"/></circle>
<circle cx="35" cy="30" r="0" fill="#22D3EE" filter="url(#hpNodeGlow)"><animate attributeName="r" values="0;6" dur="0.2s" fill="freeze" begin="0.25s"/><animate attributeName="r" values="5;7;5" dur="2s" repeatCount="indefinite" begin="1.5s"/></circle>
<circle cx="185" cy="30" r="0" fill="#22D3EE" filter="url(#hpNodeGlow)"><animate attributeName="r" values="0;6" dur="0.2s" fill="freeze" begin="0.3s"/><animate attributeName="r" values="6;5;6" dur="2s" repeatCount="indefinite" begin="1.5s"/></circle>
<circle cx="35" cy="170" r="0" fill="#6366F1"><animate attributeName="r" values="0;6" dur="0.2s" fill="freeze" begin="0.4s"/><animate attributeName="r" values="5;7;5" dur="2s" repeatCount="indefinite" begin="1.8s"/></circle>
<circle cx="185" cy="170" r="0" fill="#6366F1"><animate attributeName="r" values="0;6" dur="0.2s" fill="freeze" begin="0.45s"/><animate attributeName="r" values="6;5;6" dur="2s" repeatCount="indefinite" begin="1.8s"/></circle>
<rect x="85" y="75" width="50" height="50" rx="9" fill="url(#hpProcessorGrad)" filter="url(#hpProcessorGlow)" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.3s" fill="freeze" begin="0.45s"/></rect>
<rect x="97" y="87" width="26" height="26" rx="5" fill="#ffffff" opacity="0"><animate attributeName="opacity" values="0;0.95" dur="0.25s" fill="freeze" begin="0.55s"/></rect>
<rect x="101" y="96" width="4" height="8" rx="1" fill="#22D3EE" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.15s" fill="freeze" begin="0.7s"/><animate attributeName="height" values="8;14;6;10;8" dur="0.6s" repeatCount="indefinite" begin="1.2s"/><animate attributeName="y" values="96;93;97;95;96" dur="0.6s" repeatCount="indefinite" begin="1.2s"/></rect>
<rect x="108" y="93" width="4" height="14" rx="1" fill="#06B6D4" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.15s" fill="freeze" begin="0.75s"/><animate attributeName="height" values="14;8;12;16;14" dur="0.55s" repeatCount="indefinite" begin="1.25s"/><animate attributeName="y" values="93;96;94;92;93" dur="0.55s" repeatCount="indefinite" begin="1.25s"/></rect>
<rect x="115" y="95" width="4" height="10" rx="1" fill="#22D3EE" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.15s" fill="freeze" begin="0.8s"/><animate attributeName="height" values="10;16;8;12;10" dur="0.5s" repeatCount="indefinite" begin="1.3s"/><animate attributeName="y" values="95;92;96;94;95" dur="0.5s" repeatCount="indefinite" begin="1.3s"/></rect>
<rect x="85" y="75" width="50" height="50" rx="9" fill="none" stroke="#22D3EE" stroke-width="1.5" opacity="0"><animate attributeName="x" values="85;70" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="y" values="75;60" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="width" values="50;80" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="height" values="50;80" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="rx" values="9;14" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="opacity" values="0.5;0" dur="2s" repeatCount="indefinite" begin="1.5s"/></rect>
</g>
<g clip-path="url(#hpTextReveal)">
<text x="280" y="175" font-family="'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-weight="700" font-size="72" fill="url(#hpShimmer)">Stratum<tspan fill="url(#hpProcessorGrad)">I</tspan>Ops</text>
</g>
<rect x="280" y="115" width="4" height="70" rx="2" fill="#22D3EE" opacity="0"><animate attributeName="opacity" values="0;1;1;0;0" dur="0.8s" fill="freeze" begin="1s" keyTimes="0;0.1;0.5;0.51;1"/><animate attributeName="x" values="280;980" dur="1s" fill="freeze" begin="1s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/></rect>
<line x1="280" y1="195" x2="280" y2="195" stroke="url(#hpUnderlineGrad)" stroke-width="3" stroke-linecap="round" opacity="0"><animate attributeName="opacity" values="0;0.7" dur="0.1s" fill="freeze" begin="1.4s"/><animate attributeName="x2" values="280;750" dur="0.8s" fill="freeze" begin="1.4s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/></line>
</g>
<!-- ─── LEGEND (y=718, x=45, w=270) ─── -->
<rect x="45" y="718" width="270" height="88" rx="6" fill="#0c1018" stroke="#1E293B" stroke-width=".8"/>
<text x="180" y="736" text-anchor="middle" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8" letter-spacing="2">LEGEND</text>
<line x1="55" y1="750" x2="85" y2="750" stroke="#22D3EE" stroke-width="2" stroke-dasharray="6 3"/>
<text x="91" y="754" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">event flow (NATS)</text>
<line x1="55" y1="768" x2="85" y2="768" stroke="#F59E0B" stroke-width="2" stroke-dasharray="6 3"/>
<text x="91" y="772" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">auth / credentials</text>
<line x1="55" y1="786" x2="85" y2="786" stroke="#10B981" stroke-width="2" stroke-dasharray="6 3"/>
<text x="91" y="790" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">execution</text>
<line x1="213" y1="750" x2="243" y2="750" stroke="#A78BFA" stroke-width="2" stroke-dasharray="6 3"/>
<text x="249" y="754" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">state (DB)</text>
<line x1="213" y1="768" x2="243" y2="768" stroke="#06B6D4" stroke-width="2" stroke-dasharray="6 3"/>
<text x="249" y="772" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">OCI / registry</text>
<line x1="213" y1="786" x2="243" y2="786" stroke="#64748B" stroke-width="1.5" stroke-dasharray="3 5"/>
<text x="249" y="790" fill="#94A3B8" font-family="Inter,sans-serif" font-size="9">optional</text>
<!-- ─── BRANDING ─── -->
<text x="1140" y="817" text-anchor="end" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="9">stratumiops · v0.1</text>
</svg>

After

Width:  |  Height:  |  Size: 41 KiB

View file

@ -0,0 +1,469 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1170 830">
<defs>
<style>
.flow { animation: flow-a 3s linear infinite; }
.flow-fast { animation: flow-a 1.8s linear infinite; }
.flow-slow { animation: flow-a 5s linear infinite; }
.flow-vslow { animation: flow-a 7s linear infinite; }
@keyframes flow-a { from { stroke-dashoffset: 24; } to { stroke-dashoffset: 0; } }
.orbit { animation: orbit-a 8s linear infinite; }
@keyframes orbit-a { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -68; } }
.glow-pulse { animation: glow-a 3s ease-in-out infinite; }
@keyframes glow-a { 0%,100% { opacity:.08; } 50% { opacity:.28; } }
.hb { animation: hb-a 3s ease-in-out infinite; }
.hb-d1 { animation-delay: .4s; }
.hb-d2 { animation-delay: .8s; }
.hb-d3 { animation-delay:1.2s; }
.hb-d4 { animation-delay:1.6s; }
.hb-d5 { animation-delay:2.0s; }
@keyframes hb-a { 0%,100% { opacity:.85; } 50% { opacity:1; } }
.particle-spin1 { animation: spin1 6s linear infinite; }
.particle-spin2 { animation: spin2 9s linear infinite; }
.particle-spin3 { animation: spin3 14s linear infinite; }
@keyframes spin1 { from { transform: rotate(0deg) translateX(120px); } to { transform: rotate(360deg) translateX(120px); } }
@keyframes spin2 { from { transform: rotate(120deg) translateX(120px); } to { transform: rotate(480deg) translateX(120px); } }
@keyframes spin3 { from { transform: rotate(240deg) translateX(120px); } to { transform: rotate(600deg) translateX(120px); } }
</style>
<radialGradient id="bg-grad" cx="50%" cy="42%" r="58%">
<stop offset="0%" stop-color="#F0F4F8"/>
<stop offset="100%" stop-color="#E2EAF4"/>
</radialGradient>
<linearGradient id="title-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#C7D2FE"/>
<stop offset="50%" stop-color="#E0E7FF"/>
<stop offset="100%" stop-color="#C7D2FE"/>
</linearGradient>
<linearGradient id="orch-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#EEF2FF"/>
<stop offset="100%" stop-color="#E0E7FF"/>
</linearGradient>
<linearGradient id="auth-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFFBEB"/>
<stop offset="100%" stop-color="#FEF9C3"/>
</linearGradient>
<linearGradient id="surreal-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#F5F3FF"/>
<stop offset="100%" stop-color="#EDE9FE"/>
</linearGradient>
<linearGradient id="vault-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFFBEB"/>
<stop offset="100%" stop-color="#FEF3C7"/>
</linearGradient>
<linearGradient id="oci-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ECFEFF"/>
<stop offset="100%" stop-color="#CFFAFE"/>
</linearGradient>
<linearGradient id="forgejo-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFF7ED"/>
<stop offset="100%" stop-color="#FFEDD5"/>
</linearGradient>
<linearGradient id="exec-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#F0FDF4"/>
<stop offset="100%" stop-color="#DCFCE7"/>
</linearGradient>
<linearGradient id="agent-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#F5F3FF"/>
<stop offset="100%" stop-color="#EDE9FE"/>
</linearGradient>
<linearGradient id="ncl-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#6366F1" stop-opacity=".18"/>
<stop offset="50%" stop-color="#06B6D4" stop-opacity=".12"/>
<stop offset="100%" stop-color="#6366F1" stop-opacity=".18"/>
</linearGradient>
<linearGradient id="event-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFF7ED"/>
<stop offset="100%" stop-color="#FFEDD5"/>
</linearGradient>
<linearGradient id="mod-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#EEF2FF"/>
<stop offset="100%" stop-color="#E8EDFF"/>
</linearGradient>
<linearGradient id="mod-ag-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#F5F3FF"/>
<stop offset="100%" stop-color="#EDE9FE"/>
</linearGradient>
<linearGradient id="mod-pc-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ECFEFF"/>
<stop offset="100%" stop-color="#CFFAFE"/>
</linearGradient>
<linearGradient id="mod-sr-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#F0FDF4"/>
<stop offset="100%" stop-color="#DCFCE7"/>
</linearGradient>
<linearGradient id="mod-re-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFFBEB"/>
<stop offset="100%" stop-color="#FEF3C7"/>
</linearGradient>
<linearGradient id="orch-stripe" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#6366F1"/>
<stop offset="100%" stop-color="#06B6D4"/>
</linearGradient>
<filter id="shadow" x="-15%" y="-15%" width="130%" height="130%">
<feDropShadow dx="0" dy="3" stdDeviation="8" flood-color="#94A3B8" flood-opacity=".3"/>
</filter>
<filter id="glow-c" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<marker id="arr-cyan" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#0891B2"/></marker>
<marker id="arr-gold" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#D97706"/></marker>
<marker id="arr-green" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#059669"/></marker>
<marker id="arr-orange" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#EA6C00"/></marker>
<marker id="arr-purple" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#7C3AED"/></marker>
<marker id="arr-indigo" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4F46E5"/></marker>
<marker id="arr-silver" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#64748B"/></marker>
<marker id="arr-dcyan" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#0891B2"/></marker>
<!-- ─── stratumiops-h logo defs ─── -->
<linearGradient id="hpLayerGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366F1"/><stop offset="100%" style="stop-color:#4F46E5"/>
</linearGradient>
<linearGradient id="hpProcessorGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#22D3EE"/><stop offset="100%" style="stop-color:#06B6D4"/>
</linearGradient>
<linearGradient id="hpFlowGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#6366F1"/><stop offset="50%" style="stop-color:#22D3EE"/><stop offset="100%" style="stop-color:#6366F1"/>
<animate attributeName="x1" values="0%;100%;0%" dur="2.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="100%;200%;100%" dur="2.5s" repeatCount="indefinite"/>
</linearGradient>
<linearGradient id="hpUnderlineGrad" x1="280" y1="195" x2="750" y2="195" gradientUnits="userSpaceOnUse">
<stop offset="0%" style="stop-color:#6366F1"/><stop offset="50%" style="stop-color:#22D3EE"/><stop offset="100%" style="stop-color:#6366F1"/>
</linearGradient>
<linearGradient id="hpShimmer" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#6366F1"/><stop offset="40%" style="stop-color:#6366F1"/>
<stop offset="50%" style="stop-color:#22D3EE"/><stop offset="60%" style="stop-color:#6366F1"/><stop offset="100%" style="stop-color:#6366F1"/>
<animate attributeName="x1" values="-100%;100%" dur="3s" repeatCount="indefinite" begin="2s"/>
<animate attributeName="x2" values="0%;200%" dur="3s" repeatCount="indefinite" begin="2s"/>
</linearGradient>
<filter id="hpProcessorGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" result="coloredBlur"><animate attributeName="stdDeviation" values="4;8;4" dur="1.5s" repeatCount="indefinite"/></feGaussianBlur>
<feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="hpNodeGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<clipPath id="hpTextReveal">
<rect x="280" y="80" width="0" height="150">
<animate attributeName="width" values="0;800" dur="1s" fill="freeze" begin="1s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/>
</rect>
</clipPath>
<path id="hpPathIn1" d="M25 40 Q25 65 55 80 Q85 95 95 100" fill="none"/>
<path id="hpPathIn2" d="M215 40 Q215 65 185 80 Q155 95 145 100" fill="none"/>
<path id="hpPathOut1" d="M95 100 Q85 105 55 120 Q25 135 25 160" fill="none"/>
<path id="hpPathOut2" d="M145 100 Q155 105 185 120 Q215 135 215 160" fill="none"/>
</defs>
<!-- ─── BACKGROUND ─── -->
<rect width="1170" height="830" fill="url(#bg-grad)"/>
<g opacity=".06">
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M40 0L0 0 0 40" fill="none" stroke="#64748B" stroke-width=".5"/>
</pattern>
<rect width="1170" height="830" fill="url(#grid)"/>
</g>
<g opacity=".25">
<circle cx="90" cy="200" r=".8" fill="#94A3B8"/>
<circle cx="420" cy="88" r=".6" fill="#94A3B8"/>
<circle cx="980" cy="130" r="1" fill="#94A3B8"/>
<circle cx="55" cy="600" r=".5" fill="#94A3B8"/>
<circle cx="700" cy="800" r=".6" fill="#94A3B8"/>
</g>
<!-- ─── TITLE BAR ─── -->
<rect x="0" y="0" width="1170" height="60" fill="url(#title-grad)"/>
<line x1="0" y1="60" x2="1170" y2="60" stroke="#6366F1" stroke-width=".8" opacity=".5"/>
<text x="600" y="30" text-anchor="middle" fill="#1E40AF" font-family="'IBM Plex Mono',monospace" font-size="19" font-weight="700" letter-spacing="3" dominant-baseline="middle">STRATUM ORCHESTRATOR</text>
<text x="600" y="50" text-anchor="middle" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="9" letter-spacing="1.5">Event-Driven · Graph-Guided · Atomic Execution · Stateless · OCI-Native</text>
<!-- ─── EVENT SOURCES ROW ─── -->
<text x="400" y="82" text-anchor="middle" fill="#EA6C00" font-family="'IBM Plex Mono',monospace" font-size="8" letter-spacing="2" opacity=".9">EVENT SOURCES</text>
<rect x="55" y="90" width="128" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb"/>
<rect x="55" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="55" y="90" width="128" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".5"/>
<text x="70" y="111" fill="#1E293B" font-family="Inter,sans-serif" font-size="11" font-weight="600">provisioning</text>
<text x="70" y="127" fill="#EA6C00" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".9">emits dev.crate.&gt;</text>
<text x="70" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">crate-modified · deploy</text>
<rect x="200" y="90" width="118" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb hb-d1"/>
<rect x="200" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="200" y="90" width="118" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".5"/>
<text x="215" y="111" fill="#1E293B" font-family="Inter,sans-serif" font-size="11" font-weight="600">kogral</text>
<text x="215" y="127" fill="#EA6C00" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".9">emits dev.knowledge.&gt;</text>
<text x="215" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">node-updated · indexed</text>
<rect x="335" y="90" width="120" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb hb-d2"/>
<rect x="335" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="335" y="90" width="120" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".5"/>
<text x="350" y="111" fill="#1E293B" font-family="Inter,sans-serif" font-size="11" font-weight="600">syntaxis</text>
<text x="350" y="127" fill="#EA6C00" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".9">emits dev.project.&gt;</text>
<text x="350" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">phase · task-completed</text>
<rect x="472" y="90" width="135" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb hb-d3"/>
<rect x="472" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="472" y="90" width="135" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".5"/>
<text x="487" y="111" fill="#1E293B" font-family="Inter,sans-serif" font-size="11" font-weight="600">stratumiops</text>
<text x="487" y="127" fill="#EA6C00" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".9">emits dev.model.&gt;</text>
<text x="487" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">llm-call · embed-request</text>
<rect x="624" y="90" width="118" height="60" rx="6" fill="url(#event-grad)" filter="url(#shadow)" class="hb hb-d4"/>
<rect x="624" y="90" width="3" height="60" rx="1" fill="#F97316"/>
<rect x="624" y="90" width="118" height="60" rx="6" fill="none" stroke="#F97316" stroke-width=".8" opacity=".5"/>
<text x="639" y="111" fill="#1E293B" font-family="Inter,sans-serif" font-size="11" font-weight="600">typedialog</text>
<text x="639" y="127" fill="#EA6C00" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".9">emits dev.form.&gt;</text>
<text x="639" y="141" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="7">submitted · validated</text>
<rect x="755" y="90" width="120" height="60" rx="6" fill="none" stroke="#94A3B8" stroke-width=".8" stroke-dasharray="4 3" opacity=".6"/>
<text x="815" y="127" text-anchor="middle" fill="#94A3B8" font-family="Inter,sans-serif" font-size="19">+ more ...</text>
<!-- ─── CONNECTIONS: PROJECTS → NATS ─── -->
<line x1="119" y1="150" x2="317" y2="262" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<line x1="259" y1="150" x2="337" y2="257" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<line x1="395" y1="150" x2="355" y2="256" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<line x1="539" y1="150" x2="374" y2="258" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<line x1="686" y1="150" x2="396" y2="263" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" opacity=".7"/>
<!-- ─── NATS ORBITAL RING ─── -->
<circle cx="355" cy="385" r="129" fill="none" stroke="#0891B2" stroke-width="14" opacity=".05" class="glow-pulse"/>
<circle cx="355" cy="385" r="123" fill="none" stroke="#0891B2" stroke-width="10" opacity=".08" class="glow-pulse" filter="url(#glow-c)"/>
<circle cx="355" cy="385" r="120" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-dasharray="10 7" class="orbit"/>
<circle cx="355" cy="385" r="66" fill="none" stroke="#6366F1" stroke-width=".5" opacity=".3"/>
<radialGradient id="nats-inner" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#0891B2" stop-opacity=".14"/>
<stop offset="100%" stop-color="#6366F1" stop-opacity=".06"/>
</radialGradient>
<circle cx="355" cy="385" r="66" fill="url(#nats-inner)"/>
<text x="355" y="379" text-anchor="middle" fill="#0E7490" font-family="'IBM Plex Mono',monospace" font-size="20" font-weight="700" filter="url(#glow-c)">NATS</text>
<text x="355" y="398" text-anchor="middle" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="9">JetStream</text>
<text x="344" y="235" text-anchor="middle" fill="#0E7490" font-family="'IBM Plex Mono',monospace" font-size="9" opacity=".8">dev.&gt;</text>
<g transform="translate(355,385)">
<circle r="4" fill="#0891B2" opacity=".9" class="particle-spin1"/>
<circle r="3.5" fill="#6366F1" opacity=".7" class="particle-spin2"/>
<circle r="3" fill="#F97316" opacity=".5" class="particle-spin3"/>
</g>
<!-- ─── NATS → ORCHESTRATOR ─── -->
<path d="M 475,385 L 495,385" stroke="#0891B2" stroke-width="3" stroke-dasharray="8 4" class="flow-fast" marker-end="url(#arr-cyan)" filter="url(#glow-c)"/>
<circle r="4" fill="#0891B2" opacity=".85">
<animateMotion dur="0.25s" repeatCount="indefinite" path="M 475,385 L 495,385"/>
</circle>
<circle r="3" fill="#6366F1" opacity=".7">
<animateMotion dur="0.25s" repeatCount="indefinite" begin="0.08s" path="M 475,385 L 495,385"/>
</circle>
<!-- NKey verify badge -->
<rect x="379" y="419" width="78" height="22" rx="3" fill="#FFFBEB" stroke="#D97706" stroke-width=".7" opacity=".9"/>
<text x="418" y="429" text-anchor="middle" fill="#D97706" font-family="'IBM Plex Mono',monospace" font-size="7">NKey verify</text>
<text x="418" y="439" text-anchor="middle" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="6.5">ed25519 JWT</text>
<!-- ─── ORCHESTRATOR BOX ─── -->
<rect x="498" y="250" width="375" height="300" rx="10" fill="#CBD5E1" opacity=".25" transform="translate(3,5)"/>
<rect x="498" y="250" width="375" height="300" rx="10" fill="url(#orch-grad)" filter="url(#shadow)" class="hb"/>
<rect x="498" y="250" width="375" height="300" rx="10" fill="none" stroke="#6366F1" stroke-width=".8" opacity=".5" class="glow-pulse"/>
<rect x="498" y="250" width="4" height="300" rx="2" fill="url(#orch-stripe)"/>
<text x="685" y="273" text-anchor="middle" fill="#1E293B" font-family="Inter,sans-serif" font-size="14" font-weight="700">STRATUM ORCHESTRATOR</text>
<text x="685" y="290" text-anchor="middle" fill="#475569" font-family="Inter,sans-serif" font-size="9">Agnostic · Stateless · Graph-Guided</text>
<!-- Internal modules 2×2 -->
<!-- ActionGraph -->
<rect x="510" y="305" width="170" height="72" rx="6" fill="url(#mod-ag-grad)" stroke="#4F46E5" stroke-width=".7" opacity=".9"/>
<text x="522" y="325" fill="#4338CA" font-family="Inter,sans-serif" font-size="11" font-weight="600">◈ ActionGraph</text>
<text x="522" y="341" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">in-memory · Nickel nodes</text>
<text x="522" y="355" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">topo-sort · cycle-detect</text>
<!-- PipelineContext -->
<rect x="690" y="305" width="171" height="72" rx="6" fill="url(#mod-pc-grad)" stroke="#06B6D4" stroke-width=".7" opacity=".9"/>
<text x="702" y="325" fill="#4338CA" font-family="Inter,sans-serif" font-size="11" font-weight="600">⬡ PipelineCtx</text>
<text x="702" y="341" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">DB-first · typed caps</text>
<text x="702" y="355" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">schema-validated</text>
<!-- StageRunner -->
<rect x="510" y="389" width="170" height="130" rx="6" fill="url(#mod-sr-grad)" stroke="#059669" stroke-width=".7" opacity=".9"/>
<text x="522" y="407" fill="#4338CA" font-family="Inter,sans-serif" font-size="11" font-weight="600">▶ StageRunner</text>
<text x="522" y="423" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">JoinSet · parallel stages</text>
<text x="522" y="437" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">CancellationToken</text>
<text x="522" y="451" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">retry + backoff on failure</text>
<text x="522" y="465" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">saga compensate.nu</text>
<!-- RuleEngine -->
<rect x="690" y="389" width="171" height="130" rx="6" fill="url(#mod-re-grad)" stroke="#D97706" stroke-width=".7" opacity=".9"/>
<text x="702" y="407" fill="#4338CA" font-family="Inter,sans-serif" font-size="11" font-weight="600">⚙ RuleEngine</text>
<rect x="810" y="393" width="44" height="13" rx="3" fill="#FFFBEB" stroke="#D97706" stroke-width=".6"/>
<text x="832" y="402" text-anchor="middle" fill="#D97706" font-family="'IBM Plex Mono',monospace" font-size="7">Cedar</text>
<line x1="702" y1="415" x2="854" y2="415" stroke="#4F46E5" stroke-width=".4" opacity=".4"/>
<text x="702" y="428" fill="#D97706" font-family="'IBM Plex Mono',monospace" font-size="8" font-weight="600">◈ Cedar</text>
<text x="702" y="441" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">permit · forbid · conditions</text>
<text x="702" y="454" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">per-node authz policies</text>
<line x1="702" y1="463" x2="854" y2="463" stroke="#4F46E5" stroke-width=".4" opacity=".4"/>
<text x="702" y="476" fill="#D97706" font-family="'IBM Plex Mono',monospace" font-size="8" font-weight="600">◈ NKey</text>
<text x="702" y="489" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">ed25519 asymmetric keys</text>
<text x="702" y="502" fill="#475569" font-family="'IBM Plex Mono',monospace" font-size="8">JWT per-process · verify</text>
<text x="685" y="538" text-anchor="middle" fill="#DC2626" font-family="'IBM Plex Mono',monospace" font-size="8" opacity=".8">↺ Saga rollback on failure · compensate.nu in reverse</text>
<!-- ─── RIGHT PANEL: DATA STORES ─── -->
<path d="M 873,256 L 926,216" stroke="#7C3AED" stroke-width="1.5" stroke-dasharray="4 3" class="flow" marker-end="url(#arr-purple)" fill="none" opacity=".8"/>
<path d="M 873,350 L 926,310" stroke="#D97706" stroke-width="1.5" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-gold)" fill="none" opacity=".8"/>
<path d="M 873,444 L 926,404" stroke="#0891B2" stroke-width="1" stroke-dasharray="3 4" class="flow-slow" marker-end="url(#arr-dcyan)" fill="none" opacity=".7"/>
<!-- SurrealDB -->
<rect x="926" y="175" width="197" height="82" rx="8" fill="url(#surreal-grad)" filter="url(#shadow)" class="hb hb-d2"/>
<rect x="926" y="175" width="3" height="82" rx="1" fill="#7C3AED"/>
<rect x="926" y="175" width="197" height="82" rx="8" fill="none" stroke="#7C3AED" stroke-width=".8" opacity=".5"/>
<text x="941" y="198" fill="#7C3AED" font-family="Inter,sans-serif" font-size="12" font-weight="700">SurrealDB</text>
<text x="941" y="215" fill="#475569" font-family="Inter,sans-serif" font-size="9">Pipeline state · Step results</text>
<text x="941" y="231" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">orchestrator_state ns</text>
<text x="941" y="247" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">crash recovery</text>
<!-- SecretumVault -->
<rect x="926" y="269" width="197" height="82" rx="8" fill="url(#vault-grad)" filter="url(#shadow)" class="hb hb-d3"/>
<rect x="926" y="269" width="3" height="82" rx="1" fill="#D97706"/>
<rect x="926" y="269" width="197" height="82" rx="8" fill="none" stroke="#D97706" stroke-width=".8" opacity=".5"/>
<text x="941" y="292" fill="#D97706" font-family="Inter,sans-serif" font-size="12" font-weight="700">SecretumVault</text>
<text x="941" y="309" fill="#475569" font-family="Inter,sans-serif" font-size="9">Credentials · TTL leases</text>
<text x="941" y="325" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">vault:/secret/...</text>
<text x="941" y="341" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">never in NATS payload</text>
<!-- Zot OCI -->
<rect x="926" y="363" width="197" height="82" rx="8" fill="url(#oci-grad)" filter="url(#shadow)" class="hb hb-d4"/>
<rect x="926" y="363" width="3" height="82" rx="1" fill="#0891B2"/>
<rect x="926" y="363" width="197" height="82" rx="8" fill="none" stroke="#0891B2" stroke-width=".8" opacity=".5"/>
<text x="941" y="386" fill="#0891B2" font-family="Inter,sans-serif" font-size="12" font-weight="700">Zot OCI Registry</text>
<text x="941" y="403" fill="#475569" font-family="Inter,sans-serif" font-size="9">Node defs · Nickel libs</text>
<text x="941" y="419" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">oci://registry/nodes/</text>
<text x="941" y="435" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">content-addressed · signed</text>
<!-- ─── RIGHT PANEL: OPTIONAL SERVICES ─── -->
<text x="1042" y="533" text-anchor="middle" fill="#94A3B8" font-family="'IBM Plex Mono',monospace" font-size="8" letter-spacing="2">OPTIONAL</text>
<path d="M 873,495 C 903,495 903,559 926,559" stroke="#94A3B8" stroke-width="1" stroke-dasharray="3 5" class="flow-vslow" marker-end="url(#arr-silver)" fill="none" opacity=".6"/>
<path d="M 873,510 C 905,510 905,653 926,653" stroke="#94A3B8" stroke-width="1" stroke-dasharray="3 5" class="flow-vslow" marker-end="url(#arr-silver)" fill="none" opacity=".6"/>
<path d="M 873,525 C 907,525 907,744 926,744" stroke="#94A3B8" stroke-width="1" stroke-dasharray="3 5" class="flow-vslow" marker-end="url(#arr-silver)" fill="none" opacity=".5"/>
<!-- Kogral -->
<rect x="926" y="518" width="197" height="82" rx="8" fill="#F8FAFC" stroke="#CBD5E1" stroke-width=".8" stroke-dasharray="5 4" opacity=".9" class="hb hb-d2"/>
<text x="941" y="541" fill="#475569" font-family="Inter,sans-serif" font-size="12" font-weight="600">Kogral</text>
<text x="941" y="558" fill="#64748B" font-family="Inter,sans-serif" font-size="9">Knowledge graph</text>
<text x="941" y="574" fill="#94A3B8" font-family="'IBM Plex Mono',monospace" font-size="8">node-updated triggers</text>
<rect x="1071" y="522" width="46" height="16" rx="3" fill="#E2E8F0"/>
<text x="1094" y="533" text-anchor="middle" fill="#94A3B8" font-family="'IBM Plex Mono',monospace" font-size="7">optional</text>
<!-- Syntaxis -->
<rect x="926" y="612" width="197" height="82" rx="8" fill="#F8FAFC" stroke="#CBD5E1" stroke-width=".8" stroke-dasharray="5 4" opacity=".9" class="hb hb-d3"/>
<text x="941" y="635" fill="#475569" font-family="Inter,sans-serif" font-size="12" font-weight="600">Syntaxis</text>
<text x="941" y="652" fill="#64748B" font-family="Inter,sans-serif" font-size="9">Project orchestration</text>
<text x="941" y="668" fill="#94A3B8" font-family="'IBM Plex Mono',monospace" font-size="8">phase-transition events</text>
<rect x="1071" y="616" width="46" height="16" rx="3" fill="#E2E8F0"/>
<text x="1094" y="627" text-anchor="middle" fill="#94A3B8" font-family="'IBM Plex Mono',monospace" font-size="7">optional</text>
<!-- TypeDialog -->
<rect x="926" y="706" width="197" height="75" rx="8" fill="#F8FAFC" stroke="#CBD5E1" stroke-width=".8" stroke-dasharray="5 4" opacity=".8" class="hb hb-d4"/>
<text x="941" y="729" fill="#475569" font-family="Inter,sans-serif" font-size="12" font-weight="600">TypeDialog</text>
<text x="941" y="746" fill="#64748B" font-family="Inter,sans-serif" font-size="9">Service config UI</text>
<text x="941" y="764" fill="#94A3B8" font-family="'IBM Plex Mono',monospace" font-size="8">startup config NCL only</text>
<!-- ─── LEFT: Git repo ─── -->
<path d="M 498,545 C 120,650 50,350 180,337" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" fill="none" opacity=".6"/>
<path d="M 180,337 L 230,353" stroke="#F97316" stroke-width="1" stroke-dasharray="4 3" class="flow-slow" marker-end="url(#arr-orange)" fill="none" opacity=".7"/>
<rect x="30" y="295" width="150" height="85" rx="8" fill="url(#forgejo-grad)" filter="url(#shadow)" class="hb hb-d5"/>
<rect x="30" y="295" width="3" height="85" rx="1" fill="#F97316"/>
<rect x="30" y="295" width="150" height="85" rx="8" fill="none" stroke="#F97316" stroke-width=".8" opacity=".5"/>
<text x="45" y="318" fill="#EA6C00" font-family="Inter,sans-serif" font-size="12" font-weight="700">Git repo</text>
<text x="45" y="335" fill="#475569" font-family="Inter,sans-serif" font-size="9">Git events → NATS</text>
<text x="45" y="351" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">webhook → dev.crate.&gt;</text>
<text x="45" y="367" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">push · tag · pr</text>
<!-- ─── EXECUTION LAYER ─── -->
<path d="M 518,550 C 500,560 485,610 483,614" stroke="#059669" stroke-width="1.5" stroke-dasharray="5 3" class="flow" marker-end="url(#arr-green)" fill="none" opacity=".8"/>
<path d="M 773,550 C 755,560 717,610 714,616" stroke="#4F46E5" stroke-width="1.5" stroke-dasharray="5 3" class="flow" marker-end="url(#arr-indigo)" fill="none" opacity=".8"/>
<rect x="380" y="623" width="200" height="80" rx="8" fill="url(#exec-grad)" filter="url(#shadow)" class="hb"/>
<rect x="380" y="623" width="3" height="80" rx="1" fill="#059669"/>
<rect x="380" y="623" width="200" height="80" rx="8" fill="none" stroke="#059669" stroke-width=".8" opacity=".5"/>
<text x="395" y="644" fill="#059669" font-family="Inter,sans-serif" font-size="12" font-weight="700">Nu Executor</text>
<text x="395" y="660" fill="#475569" font-family="Inter,sans-serif" font-size="9">Atomic steps · Pure functions</text>
<text x="395" y="675" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">scripts/nu/*.nu</text>
<text x="395" y="689" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">stdout=output · exit-code=status</text>
<rect x="615" y="623" width="200" height="80" rx="8" fill="url(#agent-grad)" filter="url(#shadow)" class="hb hb-d1"/>
<rect x="615" y="623" width="3" height="80" rx="1" fill="#4F46E5"/>
<rect x="615" y="623" width="200" height="80" rx="8" fill="none" stroke="#4F46E5" stroke-width=".8" opacity=".5"/>
<text x="630" y="644" fill="#4F46E5" font-family="Inter,sans-serif" font-size="12" font-weight="700">AI Agent</text>
<text x="630" y="660" fill="#475569" font-family="Inter,sans-serif" font-size="9">stratum-llm · NATS protocol</text>
<text x="630" y="675" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">dev.agent.*.requested/responded</text>
<text x="630" y="689" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">oneshot correlation · timeout</text>
<!-- ─── NICKEL BASE LIBRARY ─── -->
<rect x="390" y="738" width="415" height="65" rx="8" fill="#F0F4FF" stroke="none"/>
<rect x="390" y="738" width="415" height="65" rx="8" fill="url(#ncl-grad)"/>
<rect x="390" y="738" width="415" height="65" rx="8" fill="none" stroke="#6366F1" stroke-width=".8" stroke-dasharray="6 4" opacity=".6"/>
<text x="597" y="761" text-anchor="middle" fill="#4338CA" font-family="Inter,sans-serif" font-size="12" font-weight="600">Nickel Base Library</text>
<text x="597" y="777" text-anchor="middle" fill="#475569" font-family="Inter,sans-serif" font-size="9">OCI-published · content-addressed · build-verified · typecheck-gated</text>
<text x="597" y="791" text-anchor="middle" fill="#64748B" font-family="'IBM Plex Mono',monospace" font-size="8">orchestrator-types.ncl · capability-schemas.ncl · defaults.ncl</text>
<!-- Zot OCI → Nickel -->
<path d="M 926,414 C 900,390 900,750 805,770" stroke="#0891B2" stroke-width="1" stroke-dasharray="3 3" class="flow-slow" marker-end="url(#arr-dcyan)" fill="none" opacity=".6"/>
<!-- ─── LOGO above LEGEND ─── -->
<g transform="translate(8,5) scale(0.2)">
<g transform="translate(20,50)">
<rect x="-220" y="18" width="220" height="24" rx="5" fill="url(#hpLayerGrad)" opacity="0.7">
<animate attributeName="x" values="-220;0" dur="0.5s" fill="freeze" calcMode="spline" keySplines="0.25 0.1 0.25 1"/>
<animate attributeName="opacity" values="0.5;0.8;0.5" dur="3s" repeatCount="indefinite" begin="1.5s"/>
</rect>
<rect x="-220" y="88" width="220" height="24" rx="5" fill="url(#hpLayerGrad)">
<animate attributeName="x" values="-220;0" dur="0.5s" fill="freeze" begin="0.15s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/>
</rect>
<rect x="-220" y="158" width="220" height="24" rx="5" fill="url(#hpLayerGrad)" opacity="0.7">
<animate attributeName="x" values="-220;0" dur="0.5s" fill="freeze" begin="0.3s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/>
<animate attributeName="opacity" values="0.8;0.5;0.8" dur="3s" repeatCount="indefinite" begin="1.5s"/>
</rect>
<path d="M35 42 Q35 65 70 85 Q95 100 95 100" stroke="url(#hpFlowGrad)" stroke-width="3" fill="none" opacity="0" stroke-linecap="round"><animate attributeName="opacity" values="0;0.6" dur="0.3s" fill="freeze" begin="0.5s"/></path>
<path d="M185 42 Q185 65 150 85 Q125 100 125 100" stroke="url(#hpFlowGrad)" stroke-width="3" fill="none" opacity="0" stroke-linecap="round"><animate attributeName="opacity" values="0;0.6" dur="0.3s" fill="freeze" begin="0.6s"/></path>
<path d="M95 100 Q95 100 70 115 Q35 135 35 158" stroke="url(#hpFlowGrad)" stroke-width="3" fill="none" opacity="0" stroke-linecap="round"><animate attributeName="opacity" values="0;0.6" dur="0.3s" fill="freeze" begin="0.7s"/></path>
<path d="M125 100 Q125 100 150 115 Q185 135 185 158" stroke="url(#hpFlowGrad)" stroke-width="3" fill="none" opacity="0" stroke-linecap="round"><animate attributeName="opacity" values="0;0.6" dur="0.3s" fill="freeze" begin="0.8s"/></path>
<circle r="4" fill="#22D3EE" filter="url(#hpNodeGlow)"><animateMotion dur="1.2s" repeatCount="indefinite" begin="1.5s"><mpath href="#hpPathIn1"/></animateMotion><animate attributeName="opacity" values="1;0.5;0.2" dur="1.2s" repeatCount="indefinite" begin="1.5s"/></circle>
<circle r="4" fill="#22D3EE" filter="url(#hpNodeGlow)"><animateMotion dur="1.2s" repeatCount="indefinite" begin="1.8s"><mpath href="#hpPathIn2"/></animateMotion><animate attributeName="opacity" values="1;0.5;0.2" dur="1.2s" repeatCount="indefinite" begin="1.8s"/></circle>
<circle r="3" fill="#6366F1"><animateMotion dur="1.2s" repeatCount="indefinite" begin="2.1s"><mpath href="#hpPathOut1"/></animateMotion><animate attributeName="opacity" values="0.2;0.5;1" dur="1.2s" repeatCount="indefinite" begin="2.1s"/></circle>
<circle r="3" fill="#6366F1"><animateMotion dur="1.2s" repeatCount="indefinite" begin="2.4s"><mpath href="#hpPathOut2"/></animateMotion><animate attributeName="opacity" values="0.2;0.5;1" dur="1.2s" repeatCount="indefinite" begin="2.4s"/></circle>
<circle cx="35" cy="30" r="0" fill="#22D3EE" filter="url(#hpNodeGlow)"><animate attributeName="r" values="0;6" dur="0.2s" fill="freeze" begin="0.25s"/><animate attributeName="r" values="5;7;5" dur="2s" repeatCount="indefinite" begin="1.5s"/></circle>
<circle cx="185" cy="30" r="0" fill="#22D3EE" filter="url(#hpNodeGlow)"><animate attributeName="r" values="0;6" dur="0.2s" fill="freeze" begin="0.3s"/><animate attributeName="r" values="6;5;6" dur="2s" repeatCount="indefinite" begin="1.5s"/></circle>
<circle cx="35" cy="170" r="0" fill="#6366F1"><animate attributeName="r" values="0;6" dur="0.2s" fill="freeze" begin="0.4s"/><animate attributeName="r" values="5;7;5" dur="2s" repeatCount="indefinite" begin="1.8s"/></circle>
<circle cx="185" cy="170" r="0" fill="#6366F1"><animate attributeName="r" values="0;6" dur="0.2s" fill="freeze" begin="0.45s"/><animate attributeName="r" values="6;5;6" dur="2s" repeatCount="indefinite" begin="1.8s"/></circle>
<rect x="85" y="75" width="50" height="50" rx="9" fill="url(#hpProcessorGrad)" filter="url(#hpProcessorGlow)" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.3s" fill="freeze" begin="0.45s"/></rect>
<rect x="97" y="87" width="26" height="26" rx="5" fill="#ffffff" opacity="0"><animate attributeName="opacity" values="0;0.95" dur="0.25s" fill="freeze" begin="0.55s"/></rect>
<rect x="101" y="96" width="4" height="8" rx="1" fill="#22D3EE" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.15s" fill="freeze" begin="0.7s"/><animate attributeName="height" values="8;14;6;10;8" dur="0.6s" repeatCount="indefinite" begin="1.2s"/><animate attributeName="y" values="96;93;97;95;96" dur="0.6s" repeatCount="indefinite" begin="1.2s"/></rect>
<rect x="108" y="93" width="4" height="14" rx="1" fill="#06B6D4" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.15s" fill="freeze" begin="0.75s"/><animate attributeName="height" values="14;8;12;16;14" dur="0.55s" repeatCount="indefinite" begin="1.25s"/><animate attributeName="y" values="93;96;94;92;93" dur="0.55s" repeatCount="indefinite" begin="1.25s"/></rect>
<rect x="115" y="95" width="4" height="10" rx="1" fill="#22D3EE" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.15s" fill="freeze" begin="0.8s"/><animate attributeName="height" values="10;16;8;12;10" dur="0.5s" repeatCount="indefinite" begin="1.3s"/><animate attributeName="y" values="95;92;96;94;95" dur="0.5s" repeatCount="indefinite" begin="1.3s"/></rect>
<rect x="85" y="75" width="50" height="50" rx="9" fill="none" stroke="#22D3EE" stroke-width="1.5" opacity="0"><animate attributeName="x" values="85;70" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="y" values="75;60" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="width" values="50;80" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="height" values="50;80" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="rx" values="9;14" dur="2s" repeatCount="indefinite" begin="1.5s"/><animate attributeName="opacity" values="0.5;0" dur="2s" repeatCount="indefinite" begin="1.5s"/></rect>
</g>
<g clip-path="url(#hpTextReveal)">
<text x="280" y="175" font-family="'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-weight="700" font-size="72" fill="url(#hpShimmer)">Stratum<tspan fill="url(#hpProcessorGrad)">I</tspan>Ops</text>
</g>
<rect x="280" y="115" width="4" height="70" rx="2" fill="#22D3EE" opacity="0"><animate attributeName="opacity" values="0;1;1;0;0" dur="0.8s" fill="freeze" begin="1s" keyTimes="0;0.1;0.5;0.51;1"/><animate attributeName="x" values="280;980" dur="1s" fill="freeze" begin="1s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/></rect>
<line x1="280" y1="195" x2="280" y2="195" stroke="url(#hpUnderlineGrad)" stroke-width="3" stroke-linecap="round" opacity="0"><animate attributeName="opacity" values="0;0.7" dur="0.1s" fill="freeze" begin="1.4s"/><animate attributeName="x2" values="280;750" dur="0.8s" fill="freeze" begin="1.4s" calcMode="spline" keySplines="0.25 0.1 0.25 1"/></line>
</g>
<!-- ─── LEGEND ─── -->
<rect x="45" y="718" width="270" height="88" rx="6" fill="#F8FAFC" stroke="#CBD5E1" stroke-width=".8"/>
<text x="180" y="736" text-anchor="middle" fill="#94A3B8" font-family="'IBM Plex Mono',monospace" font-size="8" letter-spacing="2">LEGEND</text>
<line x1="55" y1="750" x2="85" y2="750" stroke="#0891B2" stroke-width="2" stroke-dasharray="6 3"/>
<text x="91" y="754" fill="#475569" font-family="Inter,sans-serif" font-size="9">event flow (NATS)</text>
<line x1="55" y1="768" x2="85" y2="768" stroke="#D97706" stroke-width="2" stroke-dasharray="6 3"/>
<text x="91" y="772" fill="#475569" font-family="Inter,sans-serif" font-size="9">auth / credentials</text>
<line x1="55" y1="786" x2="85" y2="786" stroke="#059669" stroke-width="2" stroke-dasharray="6 3"/>
<text x="91" y="790" fill="#475569" font-family="Inter,sans-serif" font-size="9">execution</text>
<line x1="213" y1="750" x2="243" y2="750" stroke="#7C3AED" stroke-width="2" stroke-dasharray="6 3"/>
<text x="249" y="754" fill="#475569" font-family="Inter,sans-serif" font-size="9">state (DB)</text>
<line x1="213" y1="768" x2="243" y2="768" stroke="#0891B2" stroke-width="2" stroke-dasharray="6 3"/>
<text x="249" y="772" fill="#475569" font-family="Inter,sans-serif" font-size="9">OCI / registry</text>
<line x1="213" y1="786" x2="243" y2="786" stroke="#94A3B8" stroke-width="1.5" stroke-dasharray="3 5"/>
<text x="249" y="790" fill="#475569" font-family="Inter,sans-serif" font-size="9">optional</text>
<!-- ─── BRANDING ─── -->
<text x="1140" y="817" text-anchor="end" fill="#94A3B8" font-family="'IBM Plex Mono',monospace" font-size="9">stratumiops · v0.1</text>
</svg>

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -0,0 +1,372 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=JetBrains+Mono:wght@400;500;700&amp;display=swap');
text { font-family: 'Inter', sans-serif; }
.mono { font-family: 'JetBrains Mono', monospace; }
.title { font-size: 28px; font-weight: 700; fill: #ffffff; }
.subtitle { font-size: 14px; font-weight: 400; fill: #94a3b8; }
.section-title { font-size: 16px; font-weight: 700; fill: #ffffff; }
.label { font-size: 12px; font-weight: 600; fill: #e2e8f0; }
.label-sm { font-size: 10px; font-weight: 500; fill: #94a3b8; }
.label-mono { font-size: 11px; font-weight: 500; fill: #cbd5e1; font-family: 'JetBrains Mono', monospace; }
.badge { font-size: 9px; font-weight: 700; fill: #ffffff; }
</style>
<linearGradient id="grad-cyan-violet" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#06b6d4;stop-opacity:0.15"/>
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.05"/>
</linearGradient>
<linearGradient id="grad-violet-emerald" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:0.15"/>
<stop offset="100%" style="stop-color:#10b981;stop-opacity:0.05"/>
</linearGradient>
<linearGradient id="grad-emerald-cyan" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#10b981;stop-opacity:0.15"/>
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:0.05"/>
</linearGradient>
<linearGradient id="grad-card-cyan" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#06b6d4;stop-opacity:0.12"/>
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:0.02"/>
</linearGradient>
<linearGradient id="grad-card-violet" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:0.12"/>
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.02"/>
</linearGradient>
<linearGradient id="grad-card-emerald" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#10b981;stop-opacity:0.12"/>
<stop offset="100%" style="stop-color:#10b981;stop-opacity:0.02"/>
</linearGradient>
<linearGradient id="grad-card-amber" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:0.12"/>
<stop offset="100%" style="stop-color:#f59e0b;stop-opacity:0.02"/>
</linearGradient>
<linearGradient id="grad-header" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#06b6d4;stop-opacity:1"/>
<stop offset="50%" style="stop-color:#8b5cf6;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#10b981;stop-opacity:1"/>
</linearGradient>
<filter id="glow-sm"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="glow-md"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="glow-lg"><feGaussianBlur stdDeviation="6" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="shadow"><feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.3"/></filter>
</defs>
<!-- Background -->
<rect width="1400" height="1020" fill="#0a0a0f"/>
<rect width="1400" height="1020" fill="url(#grad-cyan-violet)" opacity="0.3"/>
<!-- Header -->
<text x="700" y="42" class="title" text-anchor="middle">SYNTAXIS Architecture</text>
<text x="700" y="62" class="subtitle" text-anchor="middle">Systematic Orchestration, Perfectly Arranged</text>
<line x1="400" y1="72" x2="1000" y2="72" stroke="url(#grad-header)" stroke-width="1" opacity="0.4"/>
<!-- ======================== -->
<!-- USER INTERFACES (Top) -->
<!-- ======================== -->
<g transform="translate(50, 90)">
<rect width="1300" height="160" rx="12" fill="url(#grad-card-cyan)" stroke="#06b6d4" stroke-width="0.8" opacity="0.9" filter="url(#shadow)"/>
<text x="20" y="28" class="section-title" fill="#06b6d4">User Interfaces</text>
<rect x="180" y="8" width="36" height="16" rx="4" fill="#06b6d4" opacity="0.2"/>
<text x="198" y="20" class="badge" fill="#06b6d4" text-anchor="middle">4</text>
<!-- CLI -->
<g transform="translate(30, 45)">
<rect width="280" height="100" rx="8" fill="#0f172a" stroke="#06b6d4" stroke-width="0.5" opacity="0.8"/>
<text x="140" y="24" class="label" text-anchor="middle" fill="#06b6d4">CLI</text>
<text x="140" y="42" class="label-mono" text-anchor="middle">syntaxis-cli</text>
<text x="140" y="60" class="label-sm" text-anchor="middle">clap structured commands</text>
<text x="140" y="76" class="label-sm" text-anchor="middle">Automation &amp; CI/CD</text>
<rect x="95" y="84" width="90" height="12" rx="3" fill="#06b6d4" opacity="0.15"/>
<text x="140" y="93" class="badge" fill="#06b6d4" text-anchor="middle">ACTIVE</text>
</g>
<!-- TUI -->
<g transform="translate(340, 45)">
<rect width="280" height="100" rx="8" fill="#0f172a" stroke="#8b5cf6" stroke-width="0.5" opacity="0.8"/>
<text x="140" y="24" class="label" text-anchor="middle" fill="#8b5cf6">TUI</text>
<text x="140" y="42" class="label-mono" text-anchor="middle">syntaxis-tui</text>
<text x="140" y="60" class="label-sm" text-anchor="middle">ratatui + vim keys (hjkl)</text>
<text x="140" y="76" class="label-sm" text-anchor="middle">Interactive &amp; SSH-friendly</text>
<rect x="95" y="84" width="90" height="12" rx="3" fill="#8b5cf6" opacity="0.15"/>
<text x="140" y="93" class="badge" fill="#8b5cf6" text-anchor="middle">ACTIVE</text>
</g>
<!-- Dashboard -->
<g transform="translate(650, 45)">
<rect width="280" height="100" rx="8" fill="#0f172a" stroke="#10b981" stroke-width="0.5" opacity="0.8"/>
<text x="140" y="24" class="label" text-anchor="middle" fill="#10b981">Dashboard</text>
<text x="140" y="42" class="label-mono" text-anchor="middle">Leptos WASM (CSR)</text>
<text x="140" y="60" class="label-sm" text-anchor="middle">Trunk + UnoCSS build</text>
<text x="140" y="76" class="label-sm" text-anchor="middle">Real-time &amp; Responsive</text>
<rect x="95" y="84" width="90" height="12" rx="3" fill="#10b981" opacity="0.15"/>
<text x="140" y="93" class="badge" fill="#10b981" text-anchor="middle">ACTIVE</text>
</g>
<!-- REST API -->
<g transform="translate(960, 45)">
<rect width="280" height="100" rx="8" fill="#0f172a" stroke="#f59e0b" stroke-width="0.5" opacity="0.8"/>
<text x="140" y="24" class="label" text-anchor="middle" fill="#f59e0b">REST API</text>
<text x="140" y="42" class="label-mono" text-anchor="middle">syntaxis-api (axum)</text>
<text x="140" y="60" class="label-sm" text-anchor="middle">Tower middleware + WebSocket</text>
<text x="140" y="76" class="label-sm" text-anchor="middle">Auth, Rate Limit, Metrics</text>
<rect x="95" y="84" width="90" height="12" rx="3" fill="#f59e0b" opacity="0.15"/>
<text x="140" y="93" class="badge" fill="#f59e0b" text-anchor="middle">ACTIVE</text>
</g>
</g>
<!-- Connection arrows from interfaces to core -->
<g filter="url(#glow-sm)">
<line x1="220" y1="255" x2="420" y2="310" stroke="#06b6d4" stroke-width="1" opacity="0.5" stroke-dasharray="4,4"/>
<line x1="530" y1="255" x2="560" y2="310" stroke="#8b5cf6" stroke-width="1" opacity="0.5" stroke-dasharray="4,4"/>
<line x1="840" y1="255" x2="750" y2="310" stroke="#10b981" stroke-width="1" opacity="0.5" stroke-dasharray="4,4"/>
<line x1="1150" y1="255" x2="900" y2="310" stroke="#f59e0b" stroke-width="1" opacity="0.5" stroke-dasharray="4,4"/>
</g>
<!-- ======================== -->
<!-- CORE ENGINE (Middle) -->
<!-- ======================== -->
<g transform="translate(50, 310)">
<rect width="860" height="290" rx="12" fill="url(#grad-card-violet)" stroke="#8b5cf6" stroke-width="0.8" opacity="0.9" filter="url(#shadow)"/>
<text x="20" y="28" class="section-title" fill="#8b5cf6">Core Engine</text>
<text x="140" y="28" class="label-mono" fill="#64748b">syntaxis-core</text>
<!-- Domain Models -->
<g transform="translate(20, 45)">
<rect width="250" height="110" rx="8" fill="#0f172a" stroke="#8b5cf6" stroke-width="0.5" opacity="0.7"/>
<text x="125" y="22" class="label" text-anchor="middle" fill="#c4b5fd">Domain Models</text>
<line x1="20" y1="30" x2="230" y2="30" stroke="#8b5cf6" stroke-width="0.3" opacity="0.3"/>
<text x="20" y="48" class="label-mono">Project</text>
<text x="100" y="48" class="label-sm">id, name, metadata</text>
<text x="20" y="64" class="label-mono">Phase</text>
<text x="100" y="64" class="label-sm">create/devel/pub/arch</text>
<text x="20" y="80" class="label-mono">Task</text>
<text x="100" y="80" class="label-sm">state, priority, audit</text>
<text x="20" y="96" class="label-mono">Template</text>
<text x="100" y="96" class="label-sm">reusable structures</text>
</g>
<!-- Phase Lifecycle -->
<g transform="translate(290, 45)">
<rect width="250" height="110" rx="8" fill="#0f172a" stroke="#06b6d4" stroke-width="0.5" opacity="0.7"/>
<text x="125" y="22" class="label" text-anchor="middle" fill="#67e8f9">Phase Lifecycle</text>
<line x1="20" y1="30" x2="230" y2="30" stroke="#06b6d4" stroke-width="0.3" opacity="0.3"/>
<!-- Phase flow diagram -->
<rect x="15" y="42" width="55" height="20" rx="4" fill="#06b6d4" opacity="0.2"/>
<text x="42" y="56" class="badge" fill="#06b6d4" text-anchor="middle">CREATE</text>
<line x1="72" y1="52" x2="82" y2="52" stroke="#06b6d4" stroke-width="0.8" marker-end="none"/>
<text x="78" y="48" class="badge" fill="#64748b">&#8594;</text>
<rect x="85" y="42" width="50" height="20" rx="4" fill="#8b5cf6" opacity="0.2"/>
<text x="110" y="56" class="badge" fill="#8b5cf6" text-anchor="middle">DEVEL</text>
<line x1="137" y1="52" x2="147" y2="52" stroke="#8b5cf6" stroke-width="0.8"/>
<text x="143" y="48" class="badge" fill="#64748b">&#8594;</text>
<rect x="150" y="42" width="50" height="20" rx="4" fill="#10b981" opacity="0.2"/>
<text x="175" y="56" class="badge" fill="#10b981" text-anchor="middle">PUBLISH</text>
<line x1="202" y1="52" x2="212" y2="52" stroke="#10b981" stroke-width="0.8"/>
<text x="208" y="48" class="badge" fill="#64748b">&#8594;</text>
<rect x="165" y="72" width="60" height="20" rx="4" fill="#f59e0b" opacity="0.2"/>
<text x="195" y="86" class="badge" fill="#f59e0b" text-anchor="middle">ARCHIVE</text>
<text x="125" y="104" class="label-sm" text-anchor="middle">State transitions with audit trail</text>
</g>
<!-- Business Logic -->
<g transform="translate(560, 45)">
<rect width="280" height="110" rx="8" fill="#0f172a" stroke="#10b981" stroke-width="0.5" opacity="0.7"/>
<text x="140" y="22" class="label" text-anchor="middle" fill="#6ee7b7">Business Logic</text>
<line x1="20" y1="30" x2="260" y2="30" stroke="#10b981" stroke-width="0.3" opacity="0.3"/>
<text x="20" y="48" class="label-mono">Audit</text>
<text x="120" y="48" class="label-sm">Full change history &amp; rollback</text>
<text x="20" y="64" class="label-mono">Checklist</text>
<text x="120" y="64" class="label-sm">Phase-based verification</text>
<text x="20" y="80" class="label-mono">Security</text>
<text x="120" y="80" class="label-sm">Auth &amp; access control</text>
<text x="20" y="96" class="label-mono">Config</text>
<text x="120" y="96" class="label-sm">TOML + Nickel driven</text>
</g>
<!-- Test badge -->
<g transform="translate(20, 170)">
<rect width="820" height="50" rx="8" fill="#0f172a" stroke="#22d3ee" stroke-width="0.3" opacity="0.5"/>
<text x="20" y="20" class="label-sm" fill="#22d3ee">Quality</text>
<rect x="70" y="8" width="100" height="16" rx="4" fill="#10b981" opacity="0.15"/>
<text x="120" y="20" class="badge" fill="#10b981" text-anchor="middle">632+ TESTS</text>
<rect x="180" y="8" width="110" height="16" rx="4" fill="#06b6d4" opacity="0.15"/>
<text x="235" y="20" class="badge" fill="#06b6d4" text-anchor="middle">ZERO UNSAFE</text>
<rect x="300" y="8" width="110" height="16" rx="4" fill="#8b5cf6" opacity="0.15"/>
<text x="355" y="20" class="badge" fill="#8b5cf6" text-anchor="middle">NO UNWRAP()</text>
<rect x="420" y="8" width="90" height="16" rx="4" fill="#f59e0b" opacity="0.15"/>
<text x="465" y="20" class="badge" fill="#f59e0b" text-anchor="middle">100% DOCS</text>
<rect x="520" y="8" width="80" height="16" rx="4" fill="#10b981" opacity="0.15"/>
<text x="560" y="20" class="badge" fill="#10b981" text-anchor="middle">CLIPPY 0W</text>
<text x="20" y="40" class="label-sm" fill="#64748b">thiserror | Result&lt;T&gt; | #![forbid(unsafe_code)] | cargo fmt | cargo audit</text>
</g>
<!-- Test distribution -->
<g transform="translate(20, 230)">
<text x="0" y="12" class="label-sm" fill="#64748b">Test distribution:</text>
<text x="120" y="12" class="label-mono" style="font-size:9px">core:173</text>
<text x="200" y="12" class="label-mono" style="font-size:9px">tui-lib:262</text>
<text x="290" y="12" class="label-mono" style="font-size:9px">api-lib:93</text>
<text x="370" y="12" class="label-mono" style="font-size:9px">vapora:57</text>
<text x="440" y="12" class="label-mono" style="font-size:9px">dashboard:52</text>
<text x="530" y="12" class="label-mono" style="font-size:9px">shared:33</text>
<text x="600" y="12" class="label-mono" style="font-size:9px">tui:10</text>
<text x="660" y="12" class="label-mono" style="font-size:9px">dash-shared:4</text>
</g>
</g>
<!-- ======================== -->
<!-- VAPORA ADAPTER (Right) -->
<!-- ======================== -->
<g transform="translate(940, 310)">
<rect width="360" height="290" rx="12" fill="url(#grad-card-amber)" stroke="#f59e0b" stroke-width="0.8" opacity="0.9" filter="url(#shadow)"/>
<text x="20" y="28" class="section-title" fill="#f59e0b">VAPORA SST</text>
<text x="140" y="28" class="label-mono" fill="#64748b">syntaxis-vapora</text>
<g transform="translate(20, 45)">
<rect width="320" height="90" rx="8" fill="#0f172a" stroke="#f59e0b" stroke-width="0.5" opacity="0.7"/>
<text x="160" y="22" class="label" text-anchor="middle" fill="#fcd34d">Orchestration Adapter</text>
<line x1="20" y1="30" x2="300" y2="30" stroke="#f59e0b" stroke-width="0.3" opacity="0.3"/>
<text x="20" y="48" class="label-sm">Agent triggering on task state changes</text>
<text x="20" y="64" class="label-sm">Phase-based workflow orchestration</text>
<text x="20" y="80" class="label-sm">Enterprise audit &amp; rollback support</text>
</g>
<g transform="translate(20, 150)">
<rect width="320" height="70" rx="8" fill="#0f172a" stroke="#f59e0b" stroke-width="0.5" opacity="0.7"/>
<text x="160" y="22" class="label" text-anchor="middle" fill="#fcd34d">Event Streaming</text>
<line x1="20" y1="30" x2="300" y2="30" stroke="#f59e0b" stroke-width="0.3" opacity="0.3"/>
<text x="20" y="48" class="label-mono">NATS</text>
<text x="80" y="48" class="label-sm">KOGRAL &amp; SYNTAXIS_SOURCE streams</text>
<text x="20" y="64" class="label-sm">Real-time federation between services</text>
</g>
<rect x="100" y="235" width="160" height="18" rx="4" fill="#f59e0b" opacity="0.15"/>
<text x="180" y="248" class="badge" fill="#f59e0b" text-anchor="middle">57 TESTS | ACTIVE</text>
</g>
<!-- ======================== -->
<!-- PERSISTENCE LAYER -->
<!-- ======================== -->
<g transform="translate(50, 620)">
<rect width="1250" height="160" rx="12" fill="url(#grad-card-emerald)" stroke="#10b981" stroke-width="0.8" opacity="0.9" filter="url(#shadow)"/>
<text x="20" y="28" class="section-title" fill="#10b981">Persistence Layer</text>
<text x="180" y="28" class="label-mono" fill="#64748b">Database trait abstraction</text>
<!-- SQLite -->
<g transform="translate(30, 45)">
<rect width="350" height="95" rx="8" fill="#0f172a" stroke="#10b981" stroke-width="0.5" opacity="0.7"/>
<text x="175" y="22" class="label" text-anchor="middle" fill="#6ee7b7">SQLite (Default)</text>
<line x1="20" y1="30" x2="330" y2="30" stroke="#10b981" stroke-width="0.3" opacity="0.3"/>
<text x="20" y="48" class="label-mono">sqlx async</text>
<text x="140" y="48" class="label-sm">Connection pooling + WAL mode</text>
<text x="20" y="64" class="label-sm">Prepared statements | Indexed queries | Batch ops</text>
<text x="20" y="82" class="label-sm" fill="#64748b">Best for: Solo dev, small teams, local</text>
</g>
<!-- SurrealDB -->
<g transform="translate(420, 45)">
<rect width="380" height="95" rx="8" fill="#0f172a" stroke="#8b5cf6" stroke-width="0.5" opacity="0.7"/>
<text x="190" y="22" class="label" text-anchor="middle" fill="#c4b5fd">SurrealDB 2.3</text>
<line x1="20" y1="30" x2="360" y2="30" stroke="#8b5cf6" stroke-width="0.3" opacity="0.3"/>
<text x="20" y="48" class="label-mono">40+ operations</text>
<text x="160" y="48" class="label-sm">Type-safe async | JSON binding</text>
<text x="20" y="64" class="label-sm">In-Memory | File (RocksDB) | Server | Docker | K8s</text>
<text x="20" y="82" class="label-sm" fill="#64748b">Best for: Teams, distributed, enterprise</text>
</g>
<!-- Config -->
<g transform="translate(840, 45)">
<rect width="380" height="95" rx="8" fill="#0f172a" stroke="#06b6d4" stroke-width="0.5" opacity="0.7"/>
<text x="190" y="22" class="label" text-anchor="middle" fill="#67e8f9">Configuration</text>
<line x1="20" y1="30" x2="360" y2="30" stroke="#06b6d4" stroke-width="0.3" opacity="0.3"/>
<text x="20" y="48" class="label-mono">TOML + Nickel</text>
<text x="160" y="48" class="label-sm">Runtime backend selection</text>
<text x="20" y="64" class="label-sm">database-default.toml | database-surrealdb.toml</text>
<text x="20" y="82" class="label-sm" fill="#64748b">Switch via: cp configs/database-*.toml configs/database.toml</text>
</g>
</g>
<!-- Connection arrows core to persistence -->
<g filter="url(#glow-sm)">
<line x1="480" y1="600" x2="350" y2="665" stroke="#10b981" stroke-width="1" opacity="0.4" stroke-dasharray="4,4"/>
<line x1="480" y1="600" x2="660" y2="665" stroke="#8b5cf6" stroke-width="1" opacity="0.4" stroke-dasharray="4,4"/>
<line x1="700" y1="600" x2="1080" y2="665" stroke="#06b6d4" stroke-width="1" opacity="0.4" stroke-dasharray="4,4"/>
</g>
<!-- ======================== -->
<!-- SHARED LIBRARIES -->
<!-- ======================== -->
<g transform="translate(50, 800)">
<rect width="1250" height="100" rx="12" fill="url(#grad-emerald-cyan)" stroke="#06b6d4" stroke-width="0.5" opacity="0.7" filter="url(#shadow)"/>
<text x="20" y="28" class="section-title" fill="#06b6d4">Shared Libraries</text>
<g transform="translate(30, 40)">
<rect width="380" height="45" rx="6" fill="#0f172a" stroke="#06b6d4" stroke-width="0.3" opacity="0.6"/>
<text x="15" y="18" class="label-mono" fill="#67e8f9">shared-api-lib</text>
<text x="15" y="35" class="label-sm">REST utilities, request/response helpers (93 tests)</text>
</g>
<g transform="translate(440, 40)">
<rect width="380" height="45" rx="6" fill="#0f172a" stroke="#8b5cf6" stroke-width="0.3" opacity="0.6"/>
<text x="15" y="18" class="label-mono" fill="#c4b5fd">rust-tui</text>
<text x="15" y="35" class="label-sm">TUI utilities, ratatui helpers (262 tests)</text>
</g>
<g transform="translate(850, 40)">
<rect width="380" height="45" rx="6" fill="#0f172a" stroke="#10b981" stroke-width="0.3" opacity="0.6"/>
<text x="15" y="18" class="label-mono" fill="#6ee7b7">tools-shared</text>
<text x="15" y="35" class="label-sm">Configuration, utilities, helpers (33 tests)</text>
</g>
</g>
<!-- ======================== -->
<!-- INFRASTRUCTURE SIDEBAR -->
<!-- ======================== -->
<g transform="translate(1310, 310)">
<rect width="80" height="580" rx="8" fill="#0f172a" stroke="#64748b" stroke-width="0.3" opacity="0.4"/>
<text x="40" y="20" class="label-sm" text-anchor="middle" fill="#64748b">Infra</text>
<g transform="translate(10, 50)">
<rect width="60" height="22" rx="4" fill="#06b6d4" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#06b6d4" text-anchor="middle">tokio</text>
</g>
<g transform="translate(10, 82)">
<rect width="60" height="22" rx="4" fill="#8b5cf6" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#8b5cf6" text-anchor="middle">axum</text>
</g>
<g transform="translate(10, 114)">
<rect width="60" height="22" rx="4" fill="#10b981" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#10b981" text-anchor="middle">sqlx</text>
</g>
<g transform="translate(10, 146)">
<rect width="60" height="22" rx="4" fill="#f59e0b" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#f59e0b" text-anchor="middle">serde</text>
</g>
<g transform="translate(10, 178)">
<rect width="60" height="22" rx="4" fill="#06b6d4" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#06b6d4" text-anchor="middle">clap</text>
</g>
<g transform="translate(10, 210)">
<rect width="60" height="22" rx="4" fill="#8b5cf6" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#8b5cf6" text-anchor="middle">ratatui</text>
</g>
<g transform="translate(10, 242)">
<rect width="60" height="22" rx="4" fill="#10b981" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#10b981" text-anchor="middle">leptos</text>
</g>
<g transform="translate(10, 274)">
<rect width="60" height="22" rx="4" fill="#f59e0b" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#f59e0b" text-anchor="middle">tower</text>
</g>
<g transform="translate(10, 306)">
<rect width="60" height="22" rx="4" fill="#06b6d4" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#06b6d4" text-anchor="middle">nats</text>
</g>
<g transform="translate(10, 338)">
<rect width="60" height="22" rx="4" fill="#8b5cf6" opacity="0.1"/>
<text x="30" y="15" class="badge" fill="#8b5cf6" text-anchor="middle">nickel</text>
</g>
</g>
<!-- Footer -->
<text x="700" y="1005" class="label-sm" text-anchor="middle" fill="#475569">SYNTAXIS v0.x | Rust 2021 (MSRV 1.75+) | 10K+ LOC | 632+ tests | Production Beta</text>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -0,0 +1,316 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=JetBrains+Mono:wght@400;500;700&amp;display=swap');
text { font-family: 'Inter', sans-serif; }
.mono { font-family: 'JetBrains Mono', monospace; }
.title { font-size: 28px; font-weight: 700; fill: #0f172a; }
.subtitle { font-size: 14px; font-weight: 400; fill: #64748b; }
.section-title { font-size: 16px; font-weight: 700; }
.label { font-size: 12px; font-weight: 600; fill: #1e293b; }
.label-sm { font-size: 10px; font-weight: 500; fill: #64748b; }
.label-mono { font-size: 11px; font-weight: 500; fill: #334155; font-family: 'JetBrains Mono', monospace; }
.badge { font-size: 9px; font-weight: 700; }
</style>
<linearGradient id="grad-cyan-violet-w" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#06b6d4;stop-opacity:0.06"/>
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.02"/>
</linearGradient>
<linearGradient id="grad-card-cyan-w" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#06b6d4;stop-opacity:0.06"/>
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:0.01"/>
</linearGradient>
<linearGradient id="grad-card-violet-w" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:0.06"/>
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.01"/>
</linearGradient>
<linearGradient id="grad-card-emerald-w" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#10b981;stop-opacity:0.06"/>
<stop offset="100%" style="stop-color:#10b981;stop-opacity:0.01"/>
</linearGradient>
<linearGradient id="grad-card-amber-w" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:0.06"/>
<stop offset="100%" style="stop-color:#f59e0b;stop-opacity:0.01"/>
</linearGradient>
<linearGradient id="grad-header-w" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#0891b2;stop-opacity:1"/>
<stop offset="50%" style="stop-color:#7c3aed;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#059669;stop-opacity:1"/>
</linearGradient>
<linearGradient id="grad-emerald-cyan-w" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#10b981;stop-opacity:0.06"/>
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:0.02"/>
</linearGradient>
<filter id="glow-sm-w"><feGaussianBlur stdDeviation="0.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="shadow-w"><feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000000" flood-opacity="0.08"/></filter>
</defs>
<!-- Background -->
<rect width="1400" height="1020" fill="#ffffff"/>
<rect width="1400" height="1020" fill="url(#grad-cyan-violet-w)" opacity="0.5"/>
<!-- Header -->
<text x="700" y="42" class="title" text-anchor="middle">SYNTAXIS Architecture</text>
<text x="700" y="62" class="subtitle" text-anchor="middle">Systematic Orchestration, Perfectly Arranged</text>
<line x1="400" y1="72" x2="1000" y2="72" stroke="url(#grad-header-w)" stroke-width="1" opacity="0.3"/>
<!-- USER INTERFACES -->
<g transform="translate(50, 90)">
<rect width="1300" height="160" rx="12" fill="url(#grad-card-cyan-w)" stroke="#0891b2" stroke-width="0.6" opacity="0.9" filter="url(#shadow-w)"/>
<text x="20" y="28" class="section-title" fill="#0891b2">User Interfaces</text>
<rect x="180" y="8" width="36" height="16" rx="4" fill="#0891b2" opacity="0.1"/>
<text x="198" y="20" class="badge" fill="#0891b2" text-anchor="middle">4</text>
<g transform="translate(30, 45)">
<rect width="280" height="100" rx="8" fill="#f8fafc" stroke="#0891b2" stroke-width="0.5" opacity="0.9"/>
<text x="140" y="24" class="label" text-anchor="middle" fill="#0891b2">CLI</text>
<text x="140" y="42" class="label-mono" text-anchor="middle">syntaxis-cli</text>
<text x="140" y="60" class="label-sm" text-anchor="middle">clap structured commands</text>
<text x="140" y="76" class="label-sm" text-anchor="middle">Automation &amp; CI/CD</text>
<rect x="95" y="84" width="90" height="12" rx="3" fill="#0891b2" opacity="0.1"/>
<text x="140" y="93" class="badge" fill="#0891b2" text-anchor="middle">ACTIVE</text>
</g>
<g transform="translate(340, 45)">
<rect width="280" height="100" rx="8" fill="#f8fafc" stroke="#7c3aed" stroke-width="0.5" opacity="0.9"/>
<text x="140" y="24" class="label" text-anchor="middle" fill="#7c3aed">TUI</text>
<text x="140" y="42" class="label-mono" text-anchor="middle">syntaxis-tui</text>
<text x="140" y="60" class="label-sm" text-anchor="middle">ratatui + vim keys (hjkl)</text>
<text x="140" y="76" class="label-sm" text-anchor="middle">Interactive &amp; SSH-friendly</text>
<rect x="95" y="84" width="90" height="12" rx="3" fill="#7c3aed" opacity="0.1"/>
<text x="140" y="93" class="badge" fill="#7c3aed" text-anchor="middle">ACTIVE</text>
</g>
<g transform="translate(650, 45)">
<rect width="280" height="100" rx="8" fill="#f8fafc" stroke="#059669" stroke-width="0.5" opacity="0.9"/>
<text x="140" y="24" class="label" text-anchor="middle" fill="#059669">Dashboard</text>
<text x="140" y="42" class="label-mono" text-anchor="middle">Leptos WASM (CSR)</text>
<text x="140" y="60" class="label-sm" text-anchor="middle">Trunk + UnoCSS build</text>
<text x="140" y="76" class="label-sm" text-anchor="middle">Real-time &amp; Responsive</text>
<rect x="95" y="84" width="90" height="12" rx="3" fill="#059669" opacity="0.1"/>
<text x="140" y="93" class="badge" fill="#059669" text-anchor="middle">ACTIVE</text>
</g>
<g transform="translate(960, 45)">
<rect width="280" height="100" rx="8" fill="#f8fafc" stroke="#d97706" stroke-width="0.5" opacity="0.9"/>
<text x="140" y="24" class="label" text-anchor="middle" fill="#d97706">REST API</text>
<text x="140" y="42" class="label-mono" text-anchor="middle">syntaxis-api (axum)</text>
<text x="140" y="60" class="label-sm" text-anchor="middle">Tower middleware + WebSocket</text>
<text x="140" y="76" class="label-sm" text-anchor="middle">Auth, Rate Limit, Metrics</text>
<rect x="95" y="84" width="90" height="12" rx="3" fill="#d97706" opacity="0.1"/>
<text x="140" y="93" class="badge" fill="#d97706" text-anchor="middle">ACTIVE</text>
</g>
</g>
<!-- Connection arrows -->
<g filter="url(#glow-sm-w)">
<line x1="220" y1="255" x2="420" y2="310" stroke="#0891b2" stroke-width="0.8" opacity="0.3" stroke-dasharray="4,4"/>
<line x1="530" y1="255" x2="560" y2="310" stroke="#7c3aed" stroke-width="0.8" opacity="0.3" stroke-dasharray="4,4"/>
<line x1="840" y1="255" x2="750" y2="310" stroke="#059669" stroke-width="0.8" opacity="0.3" stroke-dasharray="4,4"/>
<line x1="1150" y1="255" x2="900" y2="310" stroke="#d97706" stroke-width="0.8" opacity="0.3" stroke-dasharray="4,4"/>
</g>
<!-- CORE ENGINE -->
<g transform="translate(50, 310)">
<rect width="860" height="290" rx="12" fill="url(#grad-card-violet-w)" stroke="#7c3aed" stroke-width="0.6" opacity="0.9" filter="url(#shadow-w)"/>
<text x="20" y="28" class="section-title" fill="#7c3aed">Core Engine</text>
<text x="140" y="28" class="label-mono" fill="#94a3b8">syntaxis-core</text>
<g transform="translate(20, 45)">
<rect width="250" height="110" rx="8" fill="#f8fafc" stroke="#7c3aed" stroke-width="0.4" opacity="0.9"/>
<text x="125" y="22" class="label" text-anchor="middle" fill="#7c3aed">Domain Models</text>
<line x1="20" y1="30" x2="230" y2="30" stroke="#7c3aed" stroke-width="0.2" opacity="0.3"/>
<text x="20" y="48" class="label-mono">Project</text><text x="100" y="48" class="label-sm">id, name, metadata</text>
<text x="20" y="64" class="label-mono">Phase</text><text x="100" y="64" class="label-sm">create/devel/pub/arch</text>
<text x="20" y="80" class="label-mono">Task</text><text x="100" y="80" class="label-sm">state, priority, audit</text>
<text x="20" y="96" class="label-mono">Template</text><text x="100" y="96" class="label-sm">reusable structures</text>
</g>
<g transform="translate(290, 45)">
<rect width="250" height="110" rx="8" fill="#f8fafc" stroke="#0891b2" stroke-width="0.4" opacity="0.9"/>
<text x="125" y="22" class="label" text-anchor="middle" fill="#0891b2">Phase Lifecycle</text>
<line x1="20" y1="30" x2="230" y2="30" stroke="#0891b2" stroke-width="0.2" opacity="0.3"/>
<rect x="15" y="42" width="55" height="20" rx="4" fill="#0891b2" opacity="0.1"/>
<text x="42" y="56" class="badge" fill="#0891b2" text-anchor="middle">CREATE</text>
<text x="78" y="48" class="badge" fill="#94a3b8">&#8594;</text>
<rect x="85" y="42" width="50" height="20" rx="4" fill="#7c3aed" opacity="0.1"/>
<text x="110" y="56" class="badge" fill="#7c3aed" text-anchor="middle">DEVEL</text>
<text x="143" y="48" class="badge" fill="#94a3b8">&#8594;</text>
<rect x="150" y="42" width="50" height="20" rx="4" fill="#059669" opacity="0.1"/>
<text x="175" y="56" class="badge" fill="#059669" text-anchor="middle">PUBLISH</text>
<rect x="165" y="72" width="60" height="20" rx="4" fill="#d97706" opacity="0.1"/>
<text x="195" y="86" class="badge" fill="#d97706" text-anchor="middle">ARCHIVE</text>
<text x="125" y="104" class="label-sm" text-anchor="middle">State transitions with audit trail</text>
</g>
<g transform="translate(560, 45)">
<rect width="280" height="110" rx="8" fill="#f8fafc" stroke="#059669" stroke-width="0.4" opacity="0.9"/>
<text x="140" y="22" class="label" text-anchor="middle" fill="#059669">Business Logic</text>
<line x1="20" y1="30" x2="260" y2="30" stroke="#059669" stroke-width="0.2" opacity="0.3"/>
<text x="20" y="48" class="label-mono">Audit</text><text x="120" y="48" class="label-sm">Full change history &amp; rollback</text>
<text x="20" y="64" class="label-mono">Checklist</text><text x="120" y="64" class="label-sm">Phase-based verification</text>
<text x="20" y="80" class="label-mono">Security</text><text x="120" y="80" class="label-sm">Auth &amp; access control</text>
<text x="20" y="96" class="label-mono">Config</text><text x="120" y="96" class="label-sm">TOML + Nickel driven</text>
</g>
<g transform="translate(20, 170)">
<rect width="820" height="50" rx="8" fill="#f8fafc" stroke="#0891b2" stroke-width="0.2" opacity="0.7"/>
<text x="20" y="20" class="label-sm" fill="#0891b2">Quality</text>
<rect x="70" y="8" width="100" height="16" rx="4" fill="#059669" opacity="0.08"/>
<text x="120" y="20" class="badge" fill="#059669" text-anchor="middle">632+ TESTS</text>
<rect x="180" y="8" width="110" height="16" rx="4" fill="#0891b2" opacity="0.08"/>
<text x="235" y="20" class="badge" fill="#0891b2" text-anchor="middle">ZERO UNSAFE</text>
<rect x="300" y="8" width="110" height="16" rx="4" fill="#7c3aed" opacity="0.08"/>
<text x="355" y="20" class="badge" fill="#7c3aed" text-anchor="middle">NO UNWRAP()</text>
<rect x="420" y="8" width="90" height="16" rx="4" fill="#d97706" opacity="0.08"/>
<text x="465" y="20" class="badge" fill="#d97706" text-anchor="middle">100% DOCS</text>
<rect x="520" y="8" width="80" height="16" rx="4" fill="#059669" opacity="0.08"/>
<text x="560" y="20" class="badge" fill="#059669" text-anchor="middle">CLIPPY 0W</text>
<text x="20" y="40" class="label-sm" fill="#94a3b8">thiserror | Result&lt;T&gt; | #![forbid(unsafe_code)] | cargo fmt | cargo audit</text>
</g>
<g transform="translate(20, 230)">
<text x="0" y="12" class="label-sm" fill="#94a3b8">Test distribution:</text>
<text x="120" y="12" class="label-mono" style="font-size:9px">core:173</text>
<text x="200" y="12" class="label-mono" style="font-size:9px">tui-lib:262</text>
<text x="290" y="12" class="label-mono" style="font-size:9px">api-lib:93</text>
<text x="370" y="12" class="label-mono" style="font-size:9px">vapora:57</text>
<text x="440" y="12" class="label-mono" style="font-size:9px">dashboard:52</text>
<text x="530" y="12" class="label-mono" style="font-size:9px">shared:33</text>
<text x="600" y="12" class="label-mono" style="font-size:9px">tui:10</text>
<text x="660" y="12" class="label-mono" style="font-size:9px">dash-shared:4</text>
</g>
</g>
<!-- VAPORA SST -->
<g transform="translate(940, 310)">
<rect width="360" height="290" rx="12" fill="url(#grad-card-amber-w)" stroke="#d97706" stroke-width="0.6" opacity="0.9" filter="url(#shadow-w)"/>
<text x="20" y="28" class="section-title" fill="#d97706">VAPORA SST</text>
<text x="140" y="28" class="label-mono" fill="#94a3b8">syntaxis-vapora</text>
<g transform="translate(20, 45)">
<rect width="320" height="90" rx="8" fill="#f8fafc" stroke="#d97706" stroke-width="0.4" opacity="0.9"/>
<text x="160" y="22" class="label" text-anchor="middle" fill="#d97706">Orchestration Adapter</text>
<line x1="20" y1="30" x2="300" y2="30" stroke="#d97706" stroke-width="0.2" opacity="0.3"/>
<text x="20" y="48" class="label-sm">Agent triggering on task state changes</text>
<text x="20" y="64" class="label-sm">Phase-based workflow orchestration</text>
<text x="20" y="80" class="label-sm">Enterprise audit &amp; rollback support</text>
</g>
<g transform="translate(20, 150)">
<rect width="320" height="70" rx="8" fill="#f8fafc" stroke="#d97706" stroke-width="0.4" opacity="0.9"/>
<text x="160" y="22" class="label" text-anchor="middle" fill="#d97706">Event Streaming</text>
<line x1="20" y1="30" x2="300" y2="30" stroke="#d97706" stroke-width="0.2" opacity="0.3"/>
<text x="20" y="48" class="label-mono">NATS</text>
<text x="80" y="48" class="label-sm">KOGRAL &amp; SYNTAXIS_SOURCE streams</text>
<text x="20" y="64" class="label-sm">Real-time federation between services</text>
</g>
<rect x="100" y="235" width="160" height="18" rx="4" fill="#d97706" opacity="0.08"/>
<text x="180" y="248" class="badge" fill="#d97706" text-anchor="middle">57 TESTS | ACTIVE</text>
</g>
<!-- PERSISTENCE LAYER -->
<g transform="translate(50, 620)">
<rect width="1250" height="160" rx="12" fill="url(#grad-card-emerald-w)" stroke="#059669" stroke-width="0.6" opacity="0.9" filter="url(#shadow-w)"/>
<text x="20" y="28" class="section-title" fill="#059669">Persistence Layer</text>
<text x="180" y="28" class="label-mono" fill="#94a3b8">Database trait abstraction</text>
<g transform="translate(30, 45)">
<rect width="350" height="95" rx="8" fill="#f8fafc" stroke="#059669" stroke-width="0.4" opacity="0.9"/>
<text x="175" y="22" class="label" text-anchor="middle" fill="#059669">SQLite (Default)</text>
<line x1="20" y1="30" x2="330" y2="30" stroke="#059669" stroke-width="0.2" opacity="0.3"/>
<text x="20" y="48" class="label-mono">sqlx async</text><text x="140" y="48" class="label-sm">Connection pooling + WAL mode</text>
<text x="20" y="64" class="label-sm">Prepared statements | Indexed queries | Batch ops</text>
<text x="20" y="82" class="label-sm" fill="#94a3b8">Best for: Solo dev, small teams, local</text>
</g>
<g transform="translate(420, 45)">
<rect width="380" height="95" rx="8" fill="#f8fafc" stroke="#7c3aed" stroke-width="0.4" opacity="0.9"/>
<text x="190" y="22" class="label" text-anchor="middle" fill="#7c3aed">SurrealDB 2.3</text>
<line x1="20" y1="30" x2="360" y2="30" stroke="#7c3aed" stroke-width="0.2" opacity="0.3"/>
<text x="20" y="48" class="label-mono">40+ operations</text><text x="160" y="48" class="label-sm">Type-safe async | JSON binding</text>
<text x="20" y="64" class="label-sm">In-Memory | File (RocksDB) | Server | Docker | K8s</text>
<text x="20" y="82" class="label-sm" fill="#94a3b8">Best for: Teams, distributed, enterprise</text>
</g>
<g transform="translate(840, 45)">
<rect width="380" height="95" rx="8" fill="#f8fafc" stroke="#0891b2" stroke-width="0.4" opacity="0.9"/>
<text x="190" y="22" class="label" text-anchor="middle" fill="#0891b2">Configuration</text>
<line x1="20" y1="30" x2="360" y2="30" stroke="#0891b2" stroke-width="0.2" opacity="0.3"/>
<text x="20" y="48" class="label-mono">TOML + Nickel</text><text x="160" y="48" class="label-sm">Runtime backend selection</text>
<text x="20" y="64" class="label-sm">database-default.toml | database-surrealdb.toml</text>
<text x="20" y="82" class="label-sm" fill="#94a3b8">Switch via: cp configs/database-*.toml configs/database.toml</text>
</g>
</g>
<!-- Arrows core to persistence -->
<g filter="url(#glow-sm-w)">
<line x1="480" y1="600" x2="350" y2="665" stroke="#059669" stroke-width="0.8" opacity="0.25" stroke-dasharray="4,4"/>
<line x1="480" y1="600" x2="660" y2="665" stroke="#7c3aed" stroke-width="0.8" opacity="0.25" stroke-dasharray="4,4"/>
<line x1="700" y1="600" x2="1080" y2="665" stroke="#0891b2" stroke-width="0.8" opacity="0.25" stroke-dasharray="4,4"/>
</g>
<!-- SHARED LIBRARIES -->
<g transform="translate(50, 800)">
<rect width="1250" height="100" rx="12" fill="url(#grad-emerald-cyan-w)" stroke="#0891b2" stroke-width="0.4" opacity="0.7" filter="url(#shadow-w)"/>
<text x="20" y="28" class="section-title" fill="#0891b2">Shared Libraries</text>
<g transform="translate(30, 40)">
<rect width="380" height="45" rx="6" fill="#f8fafc" stroke="#0891b2" stroke-width="0.3" opacity="0.8"/>
<text x="15" y="18" class="label-mono" fill="#0891b2">shared-api-lib</text>
<text x="15" y="35" class="label-sm">REST utilities, request/response helpers (93 tests)</text>
</g>
<g transform="translate(440, 40)">
<rect width="380" height="45" rx="6" fill="#f8fafc" stroke="#7c3aed" stroke-width="0.3" opacity="0.8"/>
<text x="15" y="18" class="label-mono" fill="#7c3aed">rust-tui</text>
<text x="15" y="35" class="label-sm">TUI utilities, ratatui helpers (262 tests)</text>
</g>
<g transform="translate(850, 40)">
<rect width="380" height="45" rx="6" fill="#f8fafc" stroke="#059669" stroke-width="0.3" opacity="0.8"/>
<text x="15" y="18" class="label-mono" fill="#059669">tools-shared</text>
<text x="15" y="35" class="label-sm">Configuration, utilities, helpers (33 tests)</text>
</g>
</g>
<!-- Infrastructure sidebar -->
<g transform="translate(1310, 310)">
<rect width="80" height="580" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="0.3" opacity="0.6"/>
<text x="40" y="20" class="label-sm" text-anchor="middle" fill="#94a3b8">Infra</text>
<g transform="translate(10, 50)">
<rect width="60" height="22" rx="4" fill="#0891b2" opacity="0.06"/><text x="30" y="15" class="badge" fill="#0891b2" text-anchor="middle">tokio</text>
</g>
<g transform="translate(10, 82)">
<rect width="60" height="22" rx="4" fill="#7c3aed" opacity="0.06"/><text x="30" y="15" class="badge" fill="#7c3aed" text-anchor="middle">axum</text>
</g>
<g transform="translate(10, 114)">
<rect width="60" height="22" rx="4" fill="#059669" opacity="0.06"/><text x="30" y="15" class="badge" fill="#059669" text-anchor="middle">sqlx</text>
</g>
<g transform="translate(10, 146)">
<rect width="60" height="22" rx="4" fill="#d97706" opacity="0.06"/><text x="30" y="15" class="badge" fill="#d97706" text-anchor="middle">serde</text>
</g>
<g transform="translate(10, 178)">
<rect width="60" height="22" rx="4" fill="#0891b2" opacity="0.06"/><text x="30" y="15" class="badge" fill="#0891b2" text-anchor="middle">clap</text>
</g>
<g transform="translate(10, 210)">
<rect width="60" height="22" rx="4" fill="#7c3aed" opacity="0.06"/><text x="30" y="15" class="badge" fill="#7c3aed" text-anchor="middle">ratatui</text>
</g>
<g transform="translate(10, 242)">
<rect width="60" height="22" rx="4" fill="#059669" opacity="0.06"/><text x="30" y="15" class="badge" fill="#059669" text-anchor="middle">leptos</text>
</g>
<g transform="translate(10, 274)">
<rect width="60" height="22" rx="4" fill="#d97706" opacity="0.06"/><text x="30" y="15" class="badge" fill="#d97706" text-anchor="middle">tower</text>
</g>
<g transform="translate(10, 306)">
<rect width="60" height="22" rx="4" fill="#0891b2" opacity="0.06"/><text x="30" y="15" class="badge" fill="#0891b2" text-anchor="middle">nats</text>
</g>
<g transform="translate(10, 338)">
<rect width="60" height="22" rx="4" fill="#7c3aed" opacity="0.06"/><text x="30" y="15" class="badge" fill="#7c3aed" text-anchor="middle">nickel</text>
</g>
</g>
<!-- Footer -->
<text x="700" y="1005" class="label-sm" text-anchor="middle" fill="#94a3b8">SYNTAXIS v0.x | Rust 2021 (MSRV 1.75+) | 10K+ LOC | 632+ tests | Production Beta</text>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,253 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020" fill="none">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
text { font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif; }
.title { font-size: 28px; font-weight: 700; fill: #4f46e5; }
.subtitle { font-size: 14px; font-weight: 400; fill: #94a3b8; }
.layer-label { font-size: 18px; font-weight: 600; fill: #e2e8f0; }
.module-label { font-size: 13px; font-weight: 600; fill: #ffffff; }
.module-detail { font-size: 11px; font-weight: 400; fill: #cbd5e1; }
.backend-label { font-size: 14px; font-weight: 700; fill: #ffffff; }
.backend-detail { font-size: 11px; font-weight: 400; fill: #a5b4fc; }
.output-label { font-size: 13px; font-weight: 500; fill: #e2e8f0; }
.arrow-text { font-size: 11px; font-weight: 500; fill: #64748b; }
.phase-label { font-size: 11px; font-weight: 600; fill: #4f46e5; }
.note-text { font-size: 10px; font-weight: 400; fill: #64748b; }
.section-badge { font-size: 10px; font-weight: 700; fill: #4f46e5; letter-spacing: 0.1em; text-transform: uppercase; }
</style>
<!-- Rounded rect clip for backend boxes -->
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="1400" height="1020" rx="16" fill="#0f0f1a"/>
<rect width="1400" height="1020" rx="16" fill="url(#bgGrad)" opacity="0.4"/>
<defs>
<radialGradient id="bgGrad" cx="30%" cy="40%" r="60%">
<stop offset="0%" stop-color="#4f46e5" stop-opacity="0.08"/>
<stop offset="100%" stop-color="#0f0f1a" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Title -->
<text x="700" y="50" text-anchor="middle" class="title">TypeDialog Architecture</text>
<text x="700" y="72" text-anchor="middle" class="subtitle">Multi-Backend Form Orchestration Layer</text>
<!-- ═══════════════ LAYER 1: Form Definitions ═══════════════ -->
<text x="60" y="115" class="section-badge">FORM DEFINITIONS</text>
<rect x="40" y="125" width="1320" height="100" rx="10" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.4" stroke-width="1.5"/>
<!-- Nickel box -->
<rect x="80" y="145" width="280" height="60" rx="8" fill="#2d1b69" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="220" y="170" text-anchor="middle" class="module-label">Nickel (.ncl)</text>
<text x="220" y="188" text-anchor="middle" class="module-detail">nickel export --format json</text>
<!-- TOML box -->
<rect x="400" y="145" width="280" height="60" rx="8" fill="#1e3a5f" stroke="#3b82f6" stroke-opacity="0.6"/>
<text x="540" y="170" text-anchor="middle" class="module-label">TOML (.toml)</text>
<text x="540" y="188" text-anchor="middle" class="module-detail">serde direct deserialization</text>
<!-- load_form box -->
<rect x="780" y="145" width="540" height="60" rx="8" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.5"/>
<text x="1050" y="170" text-anchor="middle" class="module-label">load_form() &#8212; Unified Entry Point</text>
<text x="1050" y="188" text-anchor="middle" class="module-detail">Extension dispatch: .ncl &#8594; subprocess | .toml &#8594; serde | Fail-fast on errors</text>
<!-- Arrow: Form Defs -> Core -->
<line x1="700" y1="225" x2="700" y2="260" stroke="#4f46e5" stroke-opacity="0.5" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,256 700,268 706,256" fill="#4f46e5" opacity="0.6"/>
<text x="720" y="248" class="arrow-text">FormDefinition</text>
<!-- ═══════════════ LAYER 2: typedialog-core ═══════════════ -->
<text x="60" y="285" class="section-badge">TYPEDIALOG-CORE</text>
<rect x="40" y="295" width="1320" height="220" rx="10" fill="#111827" stroke="#3a3a50" stroke-opacity="0.5" stroke-width="1.5"/>
<!-- Three-Phase Execution -->
<text x="80" y="325" class="phase-label">THREE-PHASE EXECUTION</text>
<!-- Phase 1 -->
<rect x="80" y="335" width="200" height="55" rx="6" fill="#1e293b" stroke="#4f46e5" stroke-opacity="0.4"/>
<text x="180" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 1: Selectors</text>
<text x="180" y="373" text-anchor="middle" class="module-detail">Identify &amp; execute</text>
<!-- Phase 2 -->
<rect x="310" y="335" width="200" height="55" rx="6" fill="#1e293b" stroke="#4f46e5" stroke-opacity="0.4"/>
<text x="410" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 2: Element List</text>
<text x="410" y="373" text-anchor="middle" class="module-detail">Pure, no I/O</text>
<!-- Phase 3 -->
<rect x="540" y="335" width="200" height="55" rx="6" fill="#1e293b" stroke="#4f46e5" stroke-opacity="0.4"/>
<text x="640" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 3: Dispatch</text>
<text x="640" y="373" text-anchor="middle" class="module-detail">when/when_false eval</text>
<!-- Phase arrows -->
<line x1="280" y1="362" x2="310" y2="362" stroke="#4f46e5" stroke-opacity="0.4" stroke-width="1.5"/>
<polygon points="306,358 314,362 306,366" fill="#4f46e5" opacity="0.5"/>
<line x1="510" y1="362" x2="540" y2="362" stroke="#4f46e5" stroke-opacity="0.4" stroke-width="1.5"/>
<polygon points="536,358 544,362 536,366" fill="#4f46e5" opacity="0.5"/>
<!-- Core modules row -->
<text x="80" y="415" class="phase-label">CORE MODULES</text>
<!-- form_parser -->
<rect x="80" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="167" y="450" text-anchor="middle" class="module-label">form_parser</text>
<text x="167" y="466" text-anchor="middle" class="module-detail">TOML/Nickel parsing</text>
<text x="167" y="480" text-anchor="middle" class="module-detail">Field definitions</text>
<!-- validation -->
<rect x="275" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="362" y="450" text-anchor="middle" class="module-label">validation</text>
<text x="362" y="466" text-anchor="middle" class="module-detail">Nickel contracts</text>
<text x="362" y="480" text-anchor="middle" class="module-detail">Pre/post conditions</text>
<!-- i18n -->
<rect x="470" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="557" y="450" text-anchor="middle" class="module-label">i18n (Fluent)</text>
<text x="557" y="466" text-anchor="middle" class="module-detail">Locale detection</text>
<text x="557" y="480" text-anchor="middle" class="module-detail">.ftl translations</text>
<!-- encryption -->
<rect x="665" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="752" y="450" text-anchor="middle" class="module-label">encryption</text>
<text x="752" y="466" text-anchor="middle" class="module-detail">Field-level encrypt</text>
<text x="752" y="480" text-anchor="middle" class="module-detail">External services</text>
<!-- templates -->
<rect x="860" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="947" y="450" text-anchor="middle" class="module-label">templates (Tera)</text>
<text x="947" y="466" text-anchor="middle" class="module-detail">Jinja2-compatible</text>
<text x="947" y="480" text-anchor="middle" class="module-detail">Variable rendering</text>
<!-- RenderContext -->
<rect x="1055" y="335" width="270" height="55" rx="6" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.5"/>
<text x="1190" y="355" text-anchor="middle" class="module-label" style="font-size:12px">RenderContext</text>
<text x="1190" y="373" text-anchor="middle" class="module-detail">results: HashMap + locale</text>
<!-- BackendFactory -->
<rect x="1055" y="425" width="270" height="70" rx="6" fill="#2d1b69" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="1190" y="450" text-anchor="middle" class="module-label">BackendFactory</text>
<text x="1190" y="466" text-anchor="middle" class="module-detail">#[cfg(feature)] compile-time</text>
<text x="1190" y="480" text-anchor="middle" class="module-detail">+ runtime BackendType match</text>
<!-- Arrow: Core -> Backends -->
<line x1="700" y1="515" x2="700" y2="555" stroke="#4f46e5" stroke-opacity="0.5" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,551 700,563 706,551" fill="#4f46e5" opacity="0.6"/>
<text x="720" y="540" class="arrow-text">Box&lt;dyn FormBackend&gt;</text>
<text x="915" y="555" text-anchor="middle" class="note-text">trait FormBackend: Send + Sync { execute_field, execute_form_complete }</text>
<!-- ═══════════════ LAYER 3: Backends ═══════════════ -->
<text x="60" y="580" class="section-badge">BACKENDS (6 CRATES)</text>
<rect x="40" y="590" width="1320" height="130" rx="10" fill="#0d1117" stroke="#3a3a50" stroke-opacity="0.4" stroke-width="1.5"/>
<!-- CLI -->
<rect x="60" y="610" width="190" height="90" rx="8" fill="#1e3a5f" stroke="#3b82f6" stroke-opacity="0.6"/>
<text x="155" y="636" text-anchor="middle" class="backend-label">CLI</text>
<text x="155" y="654" text-anchor="middle" class="backend-detail">inquire 0.9</text>
<text x="155" y="670" text-anchor="middle" class="module-detail">Interactive prompts</text>
<text x="155" y="686" text-anchor="middle" class="module-detail">Scripts, CI/CD</text>
<!-- TUI -->
<rect x="270" y="610" width="190" height="90" rx="8" fill="#1e3a5f" stroke="#22d3ee" stroke-opacity="0.6"/>
<text x="365" y="636" text-anchor="middle" class="backend-label">TUI</text>
<text x="365" y="654" text-anchor="middle" class="backend-detail">ratatui</text>
<text x="365" y="670" text-anchor="middle" class="module-detail">Terminal UI</text>
<text x="365" y="686" text-anchor="middle" class="module-detail">Keyboard + Mouse</text>
<!-- Web -->
<rect x="480" y="610" width="190" height="90" rx="8" fill="#1e3a5f" stroke="#10b981" stroke-opacity="0.6"/>
<text x="575" y="636" text-anchor="middle" class="backend-label">Web</text>
<text x="575" y="654" text-anchor="middle" class="backend-detail">axum</text>
<text x="575" y="670" text-anchor="middle" class="module-detail">HTTP server</text>
<text x="575" y="686" text-anchor="middle" class="module-detail">Browser forms</text>
<!-- AI -->
<rect x="690" y="610" width="190" height="90" rx="8" fill="#2d1b69" stroke="#a855f7" stroke-opacity="0.6"/>
<text x="785" y="636" text-anchor="middle" class="backend-label">AI</text>
<text x="785" y="654" text-anchor="middle" class="backend-detail">RAG + embeddings</text>
<text x="785" y="670" text-anchor="middle" class="module-detail">Semantic search</text>
<text x="785" y="686" text-anchor="middle" class="module-detail">Knowledge graph</text>
<!-- Agent -->
<rect x="900" y="610" width="190" height="90" rx="8" fill="#2d1b69" stroke="#ec4899" stroke-opacity="0.6"/>
<text x="995" y="636" text-anchor="middle" class="backend-label">Agent</text>
<text x="995" y="654" text-anchor="middle" class="backend-detail">Multi-LLM execution</text>
<text x="995" y="670" text-anchor="middle" class="module-detail">.agent.mdx files</text>
<text x="995" y="686" text-anchor="middle" class="module-detail">Streaming, templates</text>
<!-- Prov-Gen -->
<rect x="1110" y="610" width="210" height="90" rx="8" fill="#1a3a2e" stroke="#10b981" stroke-opacity="0.6"/>
<text x="1215" y="636" text-anchor="middle" class="backend-label">Prov-Gen</text>
<text x="1215" y="654" text-anchor="middle" class="backend-detail">IaC generation</text>
<text x="1215" y="670" text-anchor="middle" class="module-detail">AWS, GCP, Azure</text>
<text x="1215" y="686" text-anchor="middle" class="module-detail">Hetzner, UpCloud, LXD</text>
<!-- Arrow: Backends -> Output -->
<line x1="700" y1="720" x2="700" y2="755" stroke="#4f46e5" stroke-opacity="0.5" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,751 700,763 706,751" fill="#4f46e5" opacity="0.6"/>
<text x="720" y="743" class="arrow-text">HashMap&lt;String, Value&gt;</text>
<!-- ═══════════════ LAYER 4: Output ═══════════════ -->
<text x="60" y="780" class="section-badge">OUTPUT FORMATS</text>
<rect x="40" y="790" width="640" height="70" rx="10" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="1.5"/>
<!-- Output format boxes -->
<rect x="60" y="802" width="120" height="44" rx="6" fill="#1e293b" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="120" y="829" text-anchor="middle" class="output-label">JSON</text>
<rect x="200" y="802" width="120" height="44" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-opacity="0.4"/>
<text x="260" y="829" text-anchor="middle" class="output-label">YAML</text>
<rect x="340" y="802" width="120" height="44" rx="6" fill="#1e293b" stroke="#10b981" stroke-opacity="0.4"/>
<text x="400" y="829" text-anchor="middle" class="output-label">TOML</text>
<rect x="480" y="802" width="180" height="44" rx="6" fill="#2d1b69" stroke="#7c3aed" stroke-opacity="0.5"/>
<text x="570" y="829" text-anchor="middle" class="output-label">Nickel Roundtrip</text>
<!-- ═══════════════ LAYER 4b: LLM Providers ═══════════════ -->
<text x="720" y="780" class="section-badge">LLM PROVIDERS</text>
<rect x="700" y="790" width="660" height="70" rx="10" fill="#1a1a2e" stroke="#ec4899" stroke-opacity="0.3" stroke-width="1.5"/>
<rect x="720" y="802" width="130" height="44" rx="6" fill="#1e293b" stroke="#a855f7" stroke-opacity="0.4"/>
<text x="785" y="829" text-anchor="middle" class="output-label">Claude</text>
<rect x="870" y="802" width="130" height="44" rx="6" fill="#1e293b" stroke="#10b981" stroke-opacity="0.4"/>
<text x="935" y="829" text-anchor="middle" class="output-label">OpenAI</text>
<rect x="1020" y="802" width="130" height="44" rx="6" fill="#1e293b" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="1085" y="829" text-anchor="middle" class="output-label">Gemini</text>
<rect x="1170" y="802" width="170" height="44" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-opacity="0.4"/>
<text x="1255" y="829" text-anchor="middle" class="output-label">Ollama (local)</text>
<!-- ═══════════════ LAYER 5: Integrations ═══════════════ -->
<text x="60" y="890" class="section-badge">INTEGRATIONS</text>
<rect x="40" y="900" width="1320" height="70" rx="10" fill="#0d1117" stroke="#3a3a50" stroke-opacity="0.3" stroke-width="1.5"/>
<rect x="60" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#22d3ee" stroke-opacity="0.4"/>
<text x="170" y="939" text-anchor="middle" class="output-label">Nushell Plugin</text>
<rect x="310" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="420" y="939" text-anchor="middle" class="output-label">Nickel Contracts</text>
<rect x="560" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#ec4899" stroke-opacity="0.4"/>
<text x="670" y="939" text-anchor="middle" class="output-label">Template Engine (Tera)</text>
<rect x="810" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#10b981" stroke-opacity="0.4"/>
<text x="920" y="939" text-anchor="middle" class="output-label">Multi-Cloud APIs</text>
<rect x="1060" y="912" width="280" height="44" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-opacity="0.4"/>
<text x="1200" y="939" text-anchor="middle" class="output-label">CI/CD (GitHub + Woodpecker)</text>
<!-- Stats footer -->
<text x="700" y="1000" text-anchor="middle" class="subtitle">8 crates | 6 backends | 3,818 tests | 4 output formats | 4 LLM providers | 6 cloud targets</text>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,229 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020" fill="none">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
text { font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif; }
.title { font-size: 28px; font-weight: 700; fill: #4f46e5; }
.subtitle { font-size: 14px; font-weight: 400; fill: #64748b; }
.layer-label { font-size: 18px; font-weight: 600; fill: #1e293b; }
.module-label { font-size: 13px; font-weight: 600; fill: #1e293b; }
.module-detail { font-size: 11px; font-weight: 400; fill: #475569; }
.backend-label { font-size: 14px; font-weight: 700; fill: #1e293b; }
.backend-detail { font-size: 11px; font-weight: 400; fill: #4f46e5; }
.output-label { font-size: 13px; font-weight: 500; fill: #1e293b; }
.arrow-text { font-size: 11px; font-weight: 500; fill: #94a3b8; }
.phase-label { font-size: 11px; font-weight: 600; fill: #4f46e5; letter-spacing: 0.05em; }
.note-text { font-size: 10px; font-weight: 400; fill: #94a3b8; }
.section-badge { font-size: 10px; font-weight: 700; fill: #4f46e5; letter-spacing: 0.1em; text-transform: uppercase; }
</style>
</defs>
<!-- Background -->
<rect width="1400" height="1020" rx="16" fill="#ffffff"/>
<rect width="1400" height="1020" rx="16" fill="url(#bgGradLight)" opacity="0.3"/>
<defs>
<radialGradient id="bgGradLight" cx="30%" cy="40%" r="60%">
<stop offset="0%" stop-color="#4f46e5" stop-opacity="0.04"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Title -->
<text x="700" y="50" text-anchor="middle" class="title">TypeDialog Architecture</text>
<text x="700" y="72" text-anchor="middle" class="subtitle">Multi-Backend Form Orchestration Layer</text>
<!-- ═══════════════ LAYER 1: Form Definitions ═══════════════ -->
<text x="60" y="115" class="section-badge">FORM DEFINITIONS</text>
<rect x="40" y="125" width="1320" height="100" rx="10" fill="#f8fafc" stroke="#4f46e5" stroke-opacity="0.25" stroke-width="1.5"/>
<!-- Nickel box -->
<rect x="80" y="145" width="280" height="60" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-opacity="0.4"/>
<text x="220" y="170" text-anchor="middle" class="module-label">Nickel (.ncl)</text>
<text x="220" y="188" text-anchor="middle" class="module-detail">nickel export --format json</text>
<!-- TOML box -->
<rect x="400" y="145" width="280" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="540" y="170" text-anchor="middle" class="module-label">TOML (.toml)</text>
<text x="540" y="188" text-anchor="middle" class="module-detail">serde direct deserialization</text>
<!-- load_form box -->
<rect x="780" y="145" width="540" height="60" rx="8" fill="#f8fafc" stroke="#4f46e5" stroke-opacity="0.3"/>
<text x="1050" y="170" text-anchor="middle" class="module-label">load_form() — Unified Entry Point</text>
<text x="1050" y="188" text-anchor="middle" class="module-detail">Extension dispatch: .ncl → subprocess | .toml → serde | Fail-fast on errors</text>
<!-- Arrow -->
<line x1="700" y1="225" x2="700" y2="260" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,256 700,268 706,256" fill="#4f46e5" opacity="0.4"/>
<text x="720" y="248" class="arrow-text">FormDefinition</text>
<!-- ═══════════════ LAYER 2: typedialog-core ═══════════════ -->
<text x="60" y="285" class="section-badge">TYPEDIALOG-CORE</text>
<rect x="40" y="295" width="1320" height="220" rx="10" fill="#f1f5f9" stroke="#3a3a50" stroke-opacity="0.2" stroke-width="1.5"/>
<!-- Three-Phase Execution -->
<text x="80" y="325" class="phase-label">THREE-PHASE EXECUTION</text>
<rect x="80" y="335" width="200" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.25"/>
<text x="180" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 1: Selectors</text>
<text x="180" y="373" text-anchor="middle" class="module-detail">Identify &amp; execute</text>
<rect x="310" y="335" width="200" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.25"/>
<text x="410" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 2: Element List</text>
<text x="410" y="373" text-anchor="middle" class="module-detail">Pure, no I/O</text>
<rect x="540" y="335" width="200" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.25"/>
<text x="640" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 3: Dispatch</text>
<text x="640" y="373" text-anchor="middle" class="module-detail">when/when_false eval</text>
<line x1="280" y1="362" x2="310" y2="362" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="1.5"/>
<polygon points="306,358 314,362 306,366" fill="#4f46e5" opacity="0.4"/>
<line x1="510" y1="362" x2="540" y2="362" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="1.5"/>
<polygon points="536,358 544,362 536,366" fill="#4f46e5" opacity="0.4"/>
<!-- Core modules -->
<text x="80" y="415" class="phase-label">CORE MODULES</text>
<rect x="80" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="167" y="450" text-anchor="middle" class="module-label">form_parser</text>
<text x="167" y="466" text-anchor="middle" class="module-detail">TOML/Nickel parsing</text>
<text x="167" y="480" text-anchor="middle" class="module-detail">Field definitions</text>
<rect x="275" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="362" y="450" text-anchor="middle" class="module-label">validation</text>
<text x="362" y="466" text-anchor="middle" class="module-detail">Nickel contracts</text>
<text x="362" y="480" text-anchor="middle" class="module-detail">Pre/post conditions</text>
<rect x="470" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="557" y="450" text-anchor="middle" class="module-label">i18n (Fluent)</text>
<text x="557" y="466" text-anchor="middle" class="module-detail">Locale detection</text>
<text x="557" y="480" text-anchor="middle" class="module-detail">.ftl translations</text>
<rect x="665" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="752" y="450" text-anchor="middle" class="module-label">encryption</text>
<text x="752" y="466" text-anchor="middle" class="module-detail">Field-level encrypt</text>
<text x="752" y="480" text-anchor="middle" class="module-detail">External services</text>
<rect x="860" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="947" y="450" text-anchor="middle" class="module-label">templates (Tera)</text>
<text x="947" y="466" text-anchor="middle" class="module-detail">Jinja2-compatible</text>
<text x="947" y="480" text-anchor="middle" class="module-detail">Variable rendering</text>
<!-- RenderContext -->
<rect x="1055" y="335" width="270" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.3"/>
<text x="1190" y="355" text-anchor="middle" class="module-label" style="font-size:12px">RenderContext</text>
<text x="1190" y="373" text-anchor="middle" class="module-detail">results: HashMap + locale</text>
<!-- BackendFactory -->
<rect x="1055" y="425" width="270" height="70" rx="6" fill="#ede9fe" stroke="#7c3aed" stroke-opacity="0.4"/>
<text x="1190" y="450" text-anchor="middle" class="module-label">BackendFactory</text>
<text x="1190" y="466" text-anchor="middle" class="module-detail">#[cfg(feature)] compile-time</text>
<text x="1190" y="480" text-anchor="middle" class="module-detail">+ runtime BackendType match</text>
<!-- Arrow: Core -> Backends -->
<line x1="700" y1="515" x2="700" y2="555" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,551 700,563 706,551" fill="#4f46e5" opacity="0.4"/>
<text x="720" y="540" class="arrow-text">Box&lt;dyn FormBackend&gt;</text>
<text x="915" y="555" text-anchor="middle" class="note-text">trait FormBackend: Send + Sync { execute_field, execute_form_complete }</text>
<!-- ═══════════════ LAYER 3: Backends ═══════════════ -->
<text x="60" y="580" class="section-badge">BACKENDS (6 CRATES)</text>
<rect x="40" y="590" width="1320" height="130" rx="10" fill="#fafbfc" stroke="#3a3a50" stroke-opacity="0.15" stroke-width="1.5"/>
<rect x="60" y="610" width="190" height="90" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="155" y="636" text-anchor="middle" class="backend-label">CLI</text>
<text x="155" y="654" text-anchor="middle" class="backend-detail">inquire 0.9</text>
<text x="155" y="670" text-anchor="middle" class="module-detail">Interactive prompts</text>
<text x="155" y="686" text-anchor="middle" class="module-detail">Scripts, CI/CD</text>
<rect x="270" y="610" width="190" height="90" rx="8" fill="#ecfeff" stroke="#22d3ee" stroke-opacity="0.4"/>
<text x="365" y="636" text-anchor="middle" class="backend-label">TUI</text>
<text x="365" y="654" text-anchor="middle" class="backend-detail">ratatui</text>
<text x="365" y="670" text-anchor="middle" class="module-detail">Terminal UI</text>
<text x="365" y="686" text-anchor="middle" class="module-detail">Keyboard + Mouse</text>
<rect x="480" y="610" width="190" height="90" rx="8" fill="#ecfdf5" stroke="#10b981" stroke-opacity="0.4"/>
<text x="575" y="636" text-anchor="middle" class="backend-label">Web</text>
<text x="575" y="654" text-anchor="middle" class="backend-detail">axum</text>
<text x="575" y="670" text-anchor="middle" class="module-detail">HTTP server</text>
<text x="575" y="686" text-anchor="middle" class="module-detail">Browser forms</text>
<rect x="690" y="610" width="190" height="90" rx="8" fill="#ede9fe" stroke="#a855f7" stroke-opacity="0.4"/>
<text x="785" y="636" text-anchor="middle" class="backend-label">AI</text>
<text x="785" y="654" text-anchor="middle" class="backend-detail">RAG + embeddings</text>
<text x="785" y="670" text-anchor="middle" class="module-detail">Semantic search</text>
<text x="785" y="686" text-anchor="middle" class="module-detail">Knowledge graph</text>
<rect x="900" y="610" width="190" height="90" rx="8" fill="#fdf2f8" stroke="#ec4899" stroke-opacity="0.4"/>
<text x="995" y="636" text-anchor="middle" class="backend-label">Agent</text>
<text x="995" y="654" text-anchor="middle" class="backend-detail">Multi-LLM execution</text>
<text x="995" y="670" text-anchor="middle" class="module-detail">.agent.mdx files</text>
<text x="995" y="686" text-anchor="middle" class="module-detail">Streaming, templates</text>
<rect x="1110" y="610" width="210" height="90" rx="8" fill="#ecfdf5" stroke="#10b981" stroke-opacity="0.4"/>
<text x="1215" y="636" text-anchor="middle" class="backend-label">Prov-Gen</text>
<text x="1215" y="654" text-anchor="middle" class="backend-detail">IaC generation</text>
<text x="1215" y="670" text-anchor="middle" class="module-detail">AWS, GCP, Azure</text>
<text x="1215" y="686" text-anchor="middle" class="module-detail">Hetzner, UpCloud, LXD</text>
<!-- Arrow: Backends -> Output -->
<line x1="700" y1="720" x2="700" y2="755" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,751 700,763 706,751" fill="#4f46e5" opacity="0.4"/>
<text x="720" y="743" class="arrow-text">HashMap&lt;String, Value&gt;</text>
<!-- ═══════════════ LAYER 4: Output ═══════════════ -->
<text x="60" y="780" class="section-badge">OUTPUT FORMATS</text>
<rect x="40" y="790" width="640" height="70" rx="10" fill="#f8fafc" stroke="#4f46e5" stroke-opacity="0.15" stroke-width="1.5"/>
<rect x="60" y="802" width="120" height="44" rx="6" fill="#ffffff" stroke="#3b82f6" stroke-opacity="0.3"/>
<text x="120" y="829" text-anchor="middle" class="output-label">JSON</text>
<rect x="200" y="802" width="120" height="44" rx="6" fill="#ffffff" stroke="#f59e0b" stroke-opacity="0.3"/>
<text x="260" y="829" text-anchor="middle" class="output-label">YAML</text>
<rect x="340" y="802" width="120" height="44" rx="6" fill="#ffffff" stroke="#10b981" stroke-opacity="0.3"/>
<text x="400" y="829" text-anchor="middle" class="output-label">TOML</text>
<rect x="480" y="802" width="180" height="44" rx="6" fill="#ede9fe" stroke="#7c3aed" stroke-opacity="0.3"/>
<text x="570" y="829" text-anchor="middle" class="output-label">Nickel Roundtrip</text>
<!-- ═══════════════ LAYER 4b: LLM Providers ═══════════════ -->
<text x="720" y="780" class="section-badge">LLM PROVIDERS</text>
<rect x="700" y="790" width="660" height="70" rx="10" fill="#fdf2f8" stroke="#ec4899" stroke-opacity="0.15" stroke-width="1.5"/>
<rect x="720" y="802" width="130" height="44" rx="6" fill="#ffffff" stroke="#a855f7" stroke-opacity="0.3"/>
<text x="785" y="829" text-anchor="middle" class="output-label">Claude</text>
<rect x="870" y="802" width="130" height="44" rx="6" fill="#ffffff" stroke="#10b981" stroke-opacity="0.3"/>
<text x="935" y="829" text-anchor="middle" class="output-label">OpenAI</text>
<rect x="1020" y="802" width="130" height="44" rx="6" fill="#ffffff" stroke="#3b82f6" stroke-opacity="0.3"/>
<text x="1085" y="829" text-anchor="middle" class="output-label">Gemini</text>
<rect x="1170" y="802" width="170" height="44" rx="6" fill="#ffffff" stroke="#f59e0b" stroke-opacity="0.3"/>
<text x="1255" y="829" text-anchor="middle" class="output-label">Ollama (local)</text>
<!-- ═══════════════ LAYER 5: Integrations ═══════════════ -->
<text x="60" y="890" class="section-badge">INTEGRATIONS</text>
<rect x="40" y="900" width="1320" height="70" rx="10" fill="#fafbfc" stroke="#3a3a50" stroke-opacity="0.1" stroke-width="1.5"/>
<rect x="60" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#22d3ee" stroke-opacity="0.3"/>
<text x="170" y="939" text-anchor="middle" class="output-label">Nushell Plugin</text>
<rect x="310" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.3"/>
<text x="420" y="939" text-anchor="middle" class="output-label">Nickel Contracts</text>
<rect x="560" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#ec4899" stroke-opacity="0.3"/>
<text x="670" y="939" text-anchor="middle" class="output-label">Template Engine (Tera)</text>
<rect x="810" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#10b981" stroke-opacity="0.3"/>
<text x="920" y="939" text-anchor="middle" class="output-label">Multi-Cloud APIs</text>
<rect x="1060" y="912" width="280" height="44" rx="6" fill="#ffffff" stroke="#f59e0b" stroke-opacity="0.3"/>
<text x="1200" y="939" text-anchor="middle" class="output-label">CI/CD (GitHub + Woodpecker)</text>
<!-- Stats footer -->
<text x="700" y="1000" text-anchor="middle" class="subtitle">8 crates | 6 backends | 3,818 tests | 4 output formats | 4 LLM providers | 6 cloud targets</text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,576 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&amp;family=Inter:wght@400;500;600&amp;display=swap');
</style>
<!-- Brand gradients -->
<linearGradient id="grad-main" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#22d3ee"/>
<stop offset="50%" stop-color="#a855f7"/>
<stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
<linearGradient id="grad-cyan-purple" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#22d3ee"/>
<stop offset="100%" stop-color="#a855f7"/>
</linearGradient>
<linearGradient id="grad-purple-pink" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#a855f7"/>
<stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
<linearGradient id="grad-vertical" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.8"/>
<stop offset="50%" stop-color="#a855f7" stop-opacity="0.5"/>
<stop offset="100%" stop-color="#ec4899" stop-opacity="0.3"/>
</linearGradient>
<linearGradient id="grad-card-cyan" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#22d3ee" stop-opacity="0.05"/>
</linearGradient>
<linearGradient id="grad-card-purple" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#a855f7" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#a855f7" stop-opacity="0.05"/>
</linearGradient>
<linearGradient id="grad-card-pink" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ec4899" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#ec4899" stop-opacity="0.05"/>
</linearGradient>
<!-- Glow filters -->
<filter id="glow-sm">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-md">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-lg">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glass-card">
<feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur"/>
<feOffset dx="0" dy="1" result="shadow"/>
<feFlood flood-color="#000" flood-opacity="0.3" result="color"/>
<feComposite in="color" in2="shadow" operator="in" result="shadow"/>
<feMerge><feMergeNode in="shadow"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- Animated particle marker -->
<circle id="particle-cyan" r="3" fill="#22d3ee" filter="url(#glow-md)"/>
<circle id="particle-purple" r="3" fill="#a855f7" filter="url(#glow-md)"/>
<circle id="particle-pink" r="3" fill="#ec4899" filter="url(#glow-md)"/>
<circle id="particle-sm-cyan" r="2" fill="#22d3ee" filter="url(#glow-sm)"/>
<circle id="particle-sm-purple" r="2" fill="#a855f7" filter="url(#glow-sm)"/>
<circle id="particle-sm-pink" r="2" fill="#ec4899" filter="url(#glow-sm)"/>
</defs>
<!-- ═══════════════════════════════════════ -->
<!-- BACKGROUND -->
<!-- ═══════════════════════════════════════ -->
<rect width="1400" height="1020" fill="#0a0a0a"/>
<!-- Subtle tech grid -->
<g opacity="0.04" stroke="#22d3ee" stroke-width="0.5">
<line x1="0" y1="0" x2="1400" y2="0"><animate attributeName="y1" values="0;1020" dur="60s" repeatCount="indefinite"/><animate attributeName="y2" values="0;1020" dur="60s" repeatCount="indefinite"/></line>
<line x1="100" y1="0" x2="100" y2="1020"/>
<line x1="250" y1="0" x2="250" y2="1020"/>
<line x1="400" y1="0" x2="400" y2="1020"/>
<line x1="550" y1="0" x2="550" y2="1020"/>
<line x1="700" y1="0" x2="700" y2="1020"/>
<line x1="850" y1="0" x2="850" y2="1020"/>
<line x1="1000" y1="0" x2="1000" y2="1020"/>
<line x1="1150" y1="0" x2="1150" y2="1020"/>
<line x1="1300" y1="0" x2="1300" y2="1020"/>
<line x1="0" y1="130" x2="1400" y2="130"/>
<line x1="0" y1="260" x2="1400" y2="260"/>
<line x1="0" y1="420" x2="1400" y2="420"/>
<line x1="0" y1="580" x2="1400" y2="580"/>
<line x1="0" y1="720" x2="1400" y2="720"/>
<line x1="0" y1="860" x2="1400" y2="860"/>
</g>
<!-- Ambient glow orbs -->
<circle cx="240" cy="350" r="180" fill="#22d3ee" opacity="0.07">
<animate attributeName="r" values="160;200;160" dur="8s" repeatCount="indefinite"/>
</circle>
<circle cx="690" cy="360" r="175" fill="#a855f7" opacity="0.05">
<animate attributeName="r" values="175;195;175" dur="10s" repeatCount="indefinite"/>
</circle>
<circle cx="1130" cy="350" r="200" fill="#ec4899" opacity="0.07">
<animate attributeName="r" values="180;220;180" dur="9s" repeatCount="indefinite"/>
</circle>
<!-- ═══════════════════════════════════════ -->
<!-- TITLE -->
<!-- ═══════════════════════════════════════ -->
<text x="700" y="42" font-family="'JetBrains Mono', monospace" font-size="22" font-weight="800" fill="url(#grad-main)" letter-spacing="6" text-anchor="middle" filter="url(#glow-sm)">VAPORA ARCHITECTURE</text>
<text x="700" y="62" font-family="'Inter', sans-serif" font-size="11" fill="#a855f7" opacity="0.6" letter-spacing="3" text-anchor="middle">18 CRATES · 354 TESTS · 100% RUST</text>
<!-- Layer labels (left side) -->
<text x="30" y="115" font-family="'JetBrains Mono', monospace" font-size="10" fill="#22d3ee" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 115)">PRESENTATION</text>
<text x="30" y="290" font-family="'JetBrains Mono', monospace" font-size="10" fill="#a855f7" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 290)">SERVICES</text>
<text x="30" y="485" font-family="'JetBrains Mono', monospace" font-size="10" fill="#ec4899" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 485)">INTELLIGENCE</text>
<text x="30" y="640" font-family="'JetBrains Mono', monospace" font-size="10" fill="#22d3ee" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 640)">DATA</text>
<text x="30" y="850" font-family="'JetBrains Mono', monospace" font-size="10" fill="#a855f7" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 850)">PROVIDERS</text>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 1: FRONTEND (y=80) -->
<!-- ═══════════════════════════════════════ -->
<!-- Frontend card -->
<g filter="url(#glass-card)">
<rect x="490" y="80" width="420" height="65" rx="8" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.4" stroke-width="2"/>
<rect x="494" y="80" width="412" height="1.5" rx="1" fill="url(#grad-main)" opacity="0.8"/>
<text x="700" y="106" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="#22d3ee" text-anchor="middle" filter="url(#glow-sm)">Leptos WASM Frontend</text>
<text x="700" y="124" font-family="'Inter', sans-serif" font-size="10" fill="#ffffff" opacity="0.5" text-anchor="middle">Kanban Board · Glassmorphism UI · UnoCSS · Reactive Components</text>
<!-- Pulse ring -->
<rect x="490" y="80" width="420" height="65" rx="8" fill="none" stroke="#22d3ee" stroke-opacity="0.15">
<animate attributeName="stroke-opacity" values="0.15;0.3;0.15" dur="3s" repeatCount="indefinite"/>
</rect>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- CONNECTION: Frontend → Services -->
<!-- ═══════════════════════════════════════ -->
<!-- Frontend to Backend -->
<path id="path-fe-be" d="M 580 145 L 580 160 Q 580 175 480 175 L 280 175 Q 260 175 260 195 L 260 210" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-cyan" opacity="0.8"><animateMotion dur="2.5s" repeatCount="indefinite"><mpath href="#path-fe-be"/></animateMotion></use>
<!-- Frontend to Agents -->
<line x1="700" y1="145" x2="700" y2="210" stroke="#a855f7" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-purple" opacity="0.8"><animateMotion dur="2s" repeatCount="indefinite" path="M 700 145 L 700 210"/></use>
<!-- Frontend to MCP -->
<path id="path-fe-mcp" d="M 820 145 L 820 160 Q 820 175 920 175 L 1100 175 Q 1140 175 1140 195 L 1140 210" fill="none" stroke="#ec4899" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-pink" opacity="0.8"><animateMotion dur="2.5s" repeatCount="indefinite"><mpath href="#path-fe-mcp"/></animateMotion></use>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 2: SERVICES (y=210) -->
<!-- ═══════════════════════════════════════ -->
<!-- Backend / Axum API -->
<g filter="url(#glass-card)">
<rect x="100" y="210" width="310" height="100" rx="8" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.35" stroke-width="2"/>
<rect x="104" y="210" width="302" height="1.5" rx="1" fill="#22d3ee" opacity="0.6"/>
<text x="255" y="237" font-family="'JetBrains Mono', monospace" font-size="13" font-weight="700" fill="#22d3ee" text-anchor="middle">Axum Backend API</text>
<text x="255" y="256" font-family="'Inter', sans-serif" font-size="9" fill="#ffffff" opacity="0.45" text-anchor="middle">40+ REST Endpoints · 161 Tests</text>
<!-- Sub-items -->
<g font-family="'Inter', sans-serif" font-size="8" fill="#ffffff" opacity="0.35">
<text x="120" y="278">Projects</text>
<text x="180" y="278">Tasks</text>
<text x="225" y="278">Agents</text>
<text x="280" y="278">Workflows</text>
<text x="350" y="278">Proposals</text>
<text x="120" y="294">Swarm</text>
<text x="175" y="294">Metrics</text>
<text x="235" y="294">RLM API</text>
<text x="300" y="294">WebSocket</text>
</g>
</g>
<!-- Agent Runtime -->
<g filter="url(#glass-card)">
<rect x="540" y="210" width="310" height="100" rx="8" fill="url(#grad-card-purple)" stroke="#a855f7" stroke-opacity="0.35" stroke-width="2"/>
<rect x="544" y="210" width="302" height="1.5" rx="1" fill="#a855f7" opacity="0.6"/>
<text x="695" y="237" font-family="'JetBrains Mono', monospace" font-size="13" font-weight="700" fill="#a855f7" text-anchor="middle">Agent Runtime</text>
<text x="695" y="256" font-family="'Inter', sans-serif" font-size="9" fill="#ffffff" opacity="0.45" text-anchor="middle">Orchestration · Learning Profiles · 71 Tests</text>
<g font-family="'Inter', sans-serif" font-size="8" fill="#ffffff" opacity="0.35">
<text x="560" y="278">Registry</text>
<text x="625" y="278">Coordinator</text>
<text x="720" y="278">Scoring</text>
<text x="785" y="278">Profiles</text>
<text x="560" y="294">12 Roles</text>
<text x="630" y="294">Health Checks</text>
<text x="735" y="294">Recency Bias</text>
</g>
<!-- Heartbeat animation -->
<circle cx="835" cy="225" r="4" fill="#a855f7" opacity="0.07">
<animate attributeName="r" values="3;5;3" dur="1.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.3;0.6;0.3" dur="1.5s" repeatCount="indefinite"/>
</circle>
</g>
<!-- MCP Gateway -->
<g filter="url(#glass-card)">
<rect x="980" y="210" width="310" height="100" rx="8" fill="url(#grad-card-pink)" stroke="#ec4899" stroke-opacity="0.35" stroke-width="2"/>
<rect x="984" y="210" width="302" height="1.5" rx="1" fill="#ec4899" opacity="0.6"/>
<text x="1135" y="237" font-family="'JetBrains Mono', monospace" font-size="13" font-weight="700" fill="#ec4899" text-anchor="middle">MCP Gateway</text>
<text x="1135" y="256" font-family="'Inter', sans-serif" font-size="9" fill="#ffffff" opacity="0.45" text-anchor="middle">Model Context Protocol · Plugin System</text>
<g font-family="'Inter', sans-serif" font-size="8" fill="#ffffff" opacity="0.35">
<text x="1000" y="278">Stdio Transport</text>
<text x="1105" y="278">SSE Transport</text>
<text x="1210" y="278">JSON-RPC</text>
<text x="1000" y="294">6 Tools</text>
<text x="1060" y="294">Tool Registry</text>
<text x="1160" y="294">Schema Validation</text>
</g>
</g>
<!-- A2A Protocol (small card overlapping) -->
<g filter="url(#glass-card)">
<rect x="445" y="280" width="86" height="35" rx="6" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="488" y="300" font-family="'JetBrains Mono', monospace" font-size="9" font-weight="700" fill="#a855f7" opacity="0.6" text-anchor="middle">A2A Protocol</text>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- CONNECTIONS: Services → Intelligence -->
<!-- ═══════════════════════════════════════ -->
<!-- Backend → RLM -->
<path id="path-be-rlm" d="M 200 310 L 200 380" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-cyan" opacity="0.7"><animateMotion dur="1.8s" repeatCount="indefinite" path="M 200 310 L 200 380"/></use>
<!-- Backend → SurrealDB (arco por la derecha para evitar KG) -->
<path id="path-be-sdb" d="M 320 310 L 320 340 Q 320 360 360 360 L 420 360 Q 435 360 435 380 Q 435 580 420 580 L 420 585" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.35" stroke-dasharray="4 4"/>
<!-- Agents → LLM Router -->
<line x1="695" y1="310" x2="695" y2="380" stroke="#a855f7" stroke-width="2.4" stroke-opacity="0.3"/>
<use href="#particle-purple" opacity="0.8"><animateMotion dur="1.5s" repeatCount="indefinite" path="M 695 310 L 695 380"/></use>
<use href="#particle-sm-purple" opacity="0.5"><animateMotion dur="1.5s" begin="0.75s" repeatCount="indefinite" path="M 695 310 L 695 380"/></use>
<!-- Agents → Swarm -->
<path id="path-ag-sw" d="M 800 310 L 800 340 Q 800 355 900 355 L 1050 355 Q 1070 355 1070 375 L 1070 380" fill="none" stroke="#a855f7" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-purple" opacity="0.6"><animateMotion dur="2.2s" repeatCount="indefinite"><mpath href="#path-ag-sw"/></animateMotion></use>
<!-- MCP → Agents -->
<path id="path-mcp-ag" d="M 1020 310 L 1020 320 Q 1020 332 920 332 L 810 332 Q 800 332 800 320 L 800 310" fill="none" stroke="#ec4899" stroke-width="1.6" stroke-opacity="0.35" stroke-dasharray="3 3"/>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 3: INTELLIGENCE (y=380) -->
<!-- ═══════════════════════════════════════ -->
<!-- RLM Engine -->
<g filter="url(#glass-card)">
<rect x="60" y="380" width="340" height="130" rx="8" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.4" stroke-width="1.2"/>
<rect x="64" y="380" width="332" height="2" rx="1" fill="url(#grad-main)" opacity="0.7"/>
<text x="230" y="405" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="url(#grad-main)" text-anchor="middle" filter="url(#glow-sm)">RLM Engine</text>
<text x="230" y="422" font-family="'Inter', sans-serif" font-size="9" fill="#ffffff" opacity="0.45" text-anchor="middle">Recursive Language Models · 38 Tests · 17k+ LOC</text>
<!-- RLM sub-components as mini pills -->
<g transform="translate(75, 435)">
<!-- Chunking -->
<rect x="0" y="0" width="80" height="22" rx="4" fill="rgba(34,211,238,0.12)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="40" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#22d3ee" opacity="0.8" text-anchor="middle" dominant-baseline="central">Chunking</text>
<!-- Hybrid Search -->
<rect x="90" y="0" width="95" height="22" rx="4" fill="rgba(168,85,247,0.12)" stroke="#a855f7" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="137" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#a855f7" opacity="0.8" text-anchor="middle" dominant-baseline="central">Hybrid Search</text>
<!-- Dispatcher -->
<rect x="195" y="0" width="80" height="22" rx="4" fill="rgba(236,72,153,0.12)" stroke="#ec4899" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="235" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#ec4899" opacity="0.8" text-anchor="middle" dominant-baseline="central">Dispatcher</text>
</g>
<g transform="translate(75, 465)">
<!-- BM25 -->
<rect x="0" y="0" width="65" height="20" rx="3" fill="rgba(34,211,238,0.08)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="32" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">BM25</text>
<!-- Semantic -->
<rect x="75" y="0" width="70" height="20" rx="3" fill="rgba(168,85,247,0.08)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="110" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Semantic</text>
<!-- RRF -->
<rect x="155" y="0" width="50" height="20" rx="3" fill="rgba(236,72,153,0.08)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="180" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">RRF</text>
<!-- Sandbox -->
<rect x="215" y="0" width="60" height="20" rx="3" fill="rgba(34,211,238,0.08)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="245" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Sandbox</text>
</g>
<!-- Animated search indicator -->
<g transform="translate(370, 400)">
<circle r="6" fill="none" stroke="#22d3ee" stroke-width="0.8" opacity="0.07">
<animate attributeName="r" values="4;8;4" dur="3s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.4;0.1;0.4" dur="3s" repeatCount="indefinite"/>
</circle>
<circle r="3" fill="#22d3ee" opacity="0.07">
<animate attributeName="opacity" values="0.2;0.5;0.2" dur="3s" repeatCount="indefinite"/>
</circle>
</g>
</g>
<!-- LLM Router -->
<g filter="url(#glass-card)">
<rect x="500" y="380" width="380" height="130" rx="8" fill="url(#grad-card-purple)" stroke="#a855f7" stroke-opacity="0.4" stroke-width="1.2"/>
<rect x="504" y="380" width="372" height="2" rx="1" fill="#a855f7" opacity="0.7"/>
<text x="690" y="405" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="#a855f7" text-anchor="middle" filter="url(#glow-sm)">Multi-IA LLM Router</text>
<text x="690" y="422" font-family="'Inter', sans-serif" font-size="9" fill="#ffffff" opacity="0.45" text-anchor="middle">Budget Enforcement · Cost Tracking · 53 Tests</text>
<!-- Router sub-components -->
<g transform="translate(515, 435)">
<rect x="0" y="0" width="95" height="22" rx="4" fill="rgba(168,85,247,0.12)" stroke="#a855f7" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="47" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#a855f7" opacity="0.8" text-anchor="middle" dominant-baseline="central">Rule Router</text>
<rect x="105" y="0" width="115" height="22" rx="4" fill="rgba(236,72,153,0.12)" stroke="#ec4899" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="162" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#ec4899" opacity="0.8" text-anchor="middle" dominant-baseline="central">Budget Manager</text>
<rect x="230" y="0" width="110" height="22" rx="4" fill="rgba(34,211,238,0.12)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="285" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#22d3ee" opacity="0.8" text-anchor="middle" dominant-baseline="central">Cost Tracker</text>
</g>
<g transform="translate(515, 465)">
<rect x="0" y="0" width="95" height="20" rx="3" fill="rgba(168,85,247,0.08)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="47" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Fallback Chain</text>
<rect x="105" y="0" width="90" height="20" rx="3" fill="rgba(236,72,153,0.08)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="150" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">Cost Ranker</text>
<rect x="205" y="0" width="80" height="20" rx="3" fill="rgba(34,211,238,0.08)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="245" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Providers</text>
</g>
<!-- Routing indicator animation -->
<g transform="translate(855, 400)">
<line x1="-6" y1="0" x2="6" y2="0" stroke="#a855f7" stroke-width="1.5" opacity="0.5">
<animateTransform attributeName="transform" type="rotate" values="0;360" dur="4s" repeatCount="indefinite"/>
</line>
<line x1="0" y1="-6" x2="0" y2="6" stroke="#ec4899" stroke-width="1.5" opacity="0.5">
<animateTransform attributeName="transform" type="rotate" values="0;360" dur="4s" repeatCount="indefinite"/>
</line>
</g>
</g>
<!-- Swarm Coordinator -->
<g filter="url(#glass-card)">
<rect x="980" y="380" width="310" height="130" rx="8" fill="url(#grad-card-pink)" stroke="#ec4899" stroke-opacity="0.35" stroke-width="2"/>
<rect x="984" y="380" width="302" height="1.5" rx="1" fill="#ec4899" opacity="0.6"/>
<text x="1135" y="405" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="#ec4899" text-anchor="middle" filter="url(#glow-sm)">Swarm Coordinator</text>
<text x="1135" y="422" font-family="'Inter', sans-serif" font-size="9" fill="#ffffff" opacity="0.45" text-anchor="middle">Load Balancing · Prometheus · 6 Tests</text>
<g transform="translate(995, 435)">
<rect x="0" y="0" width="90" height="22" rx="4" fill="rgba(236,72,153,0.12)" stroke="#ec4899" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="45" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#ec4899" opacity="0.8" text-anchor="middle" dominant-baseline="central">Assignment</text>
<rect x="100" y="0" width="90" height="22" rx="4" fill="rgba(34,211,238,0.12)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="145" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#22d3ee" opacity="0.8" text-anchor="middle" dominant-baseline="central">Filtering</text>
<rect x="200" y="0" width="80" height="22" rx="4" fill="rgba(168,85,247,0.12)" stroke="#a855f7" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="240" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#a855f7" opacity="0.8" text-anchor="middle" dominant-baseline="central">Metrics</text>
</g>
<g transform="translate(995, 465)">
<rect x="0" y="0" width="130" height="20" rx="3" fill="rgba(236,72,153,0.08)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="65" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">success_rate / (1+load)</text>
<rect x="140" y="0" width="80" height="20" rx="3" fill="rgba(34,211,238,0.08)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="180" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Capabilities</text>
</g>
<!-- Swarm pulse -->
<circle cx="1270" cy="395" r="3" fill="#ec4899" opacity="0.07">
<animate attributeName="r" values="2;5;2" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.2;0.5;0.2" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- RLM → LLM Router connection -->
<path id="path-rlm-llm" d="M 400 445 L 500 445" fill="none" stroke="url(#grad-cyan-purple)" stroke-width="3" stroke-opacity="0.3"/>
<use href="#particle-cyan" opacity="0.7"><animateMotion dur="1.5s" repeatCount="indefinite" path="M 400 445 L 500 445"/></use>
<!-- LLM Router → Swarm connection -->
<path d="M 880 445 L 980 445" fill="none" stroke="url(#grad-purple-pink)" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-pink" opacity="0.6"><animateMotion dur="1.8s" repeatCount="indefinite" path="M 880 445 L 980 445"/></use>
<!-- ═══════════════════════════════════════ -->
<!-- CONNECTIONS: Intelligence → Data -->
<!-- ═══════════════════════════════════════ -->
<!-- RLM → KG -->
<path id="path-rlm-kg" d="M 160 510 L 160 575" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-cyan" opacity="0.7"><animateMotion dur="1.8s" repeatCount="indefinite" path="M 160 510 L 160 575"/></use>
<!-- RLM → SurrealDB -->
<path id="path-rlm-sdb" d="M 300 510 L 300 540 Q 300 555 400 555 L 530 555 Q 545 555 545 575" fill="none" stroke="#22d3ee" stroke-width="2.4" stroke-opacity="0.25"/>
<use href="#particle-cyan" opacity="0.7"><animateMotion dur="2.5s" repeatCount="indefinite"><mpath href="#path-rlm-sdb"/></animateMotion></use>
<!-- LLM Router → Providers (goes down below data) -->
<path id="path-llm-providers" d="M 790 510 L 790 725" fill="none" stroke="#a855f7" stroke-width="3" stroke-opacity="0.2"/>
<use href="#particle-purple" opacity="0.8"><animateMotion dur="3s" repeatCount="indefinite" path="M 790 510 L 790 725"/></use>
<use href="#particle-sm-purple" opacity="0.5"><animateMotion dur="3s" begin="1.5s" repeatCount="indefinite" path="M 790 510 L 790 725"/></use>
<!-- Swarm → NATS -->
<path id="path-sw-nats" d="M 1135 510 L 1135 575" fill="none" stroke="#ec4899" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-pink" opacity="0.7"><animateMotion dur="1.8s" repeatCount="indefinite" path="M 1135 510 L 1135 575"/></use>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 4: DATA LAYER (y=575) -->
<!-- ═══════════════════════════════════════ -->
<!-- Knowledge Graph -->
<g filter="url(#glass-card)">
<rect x="60" y="575" width="260" height="100" rx="8" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="2"/>
<rect x="60" y="575" width="260" height="1.5" rx="1" fill="#22d3ee" opacity="0.5"/>
<text x="190" y="600" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#22d3ee" text-anchor="middle">Knowledge Graph</text>
<text x="190" y="617" font-family="'Inter', sans-serif" font-size="9" fill="#ffffff" opacity="0.4" text-anchor="middle">Temporal History · Learning Curves · 20 Tests</text>
<g transform="translate(75, 630)">
<rect x="0" y="0" width="80" height="18" rx="3" fill="rgba(34,211,238,0.1)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="40" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Executions</text>
<rect x="90" y="0" width="70" height="18" rx="3" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="125" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Similarity</text>
<rect x="170" y="0" width="60" height="18" rx="3" fill="rgba(236,72,153,0.1)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="200" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">Causal</text>
</g>
</g>
<!-- SurrealDB -->
<g filter="url(#glass-card)">
<rect x="420" y="575" width="340" height="100" rx="8" fill="url(#grad-card-purple)" stroke="#a855f7" stroke-opacity="0.4" stroke-width="1.2"/>
<rect x="420" y="575" width="340" height="2" rx="1" fill="url(#grad-main)" opacity="0.6"/>
<text x="590" y="600" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="url(#grad-main)" text-anchor="middle" filter="url(#glow-sm)">SurrealDB</text>
<text x="590" y="617" font-family="'Inter', sans-serif" font-size="9" fill="#ffffff" opacity="0.45" text-anchor="middle">Multi-Model Database · Multi-Tenant Scopes</text>
<g transform="translate(435, 630)">
<rect x="0" y="0" width="65" height="18" rx="3" fill="rgba(34,211,238,0.1)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="32" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Projects</text>
<rect x="75" y="0" width="50" height="18" rx="3" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="100" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Tasks</text>
<rect x="135" y="0" width="55" height="18" rx="3" fill="rgba(236,72,153,0.1)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="162" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">Agents</text>
<rect x="200" y="0" width="55" height="18" rx="3" fill="rgba(34,211,238,0.1)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="227" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Chunks</text>
<rect x="265" y="0" width="45" height="18" rx="3" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="287" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">A2A</text>
</g>
<!-- DB activity indicator -->
<g transform="translate(735, 590)">
<rect x="0" y="0" width="2" height="10" fill="#22d3ee" opacity="0.4">
<animate attributeName="height" values="6;12;6" dur="1.2s" repeatCount="indefinite"/>
</rect>
<rect x="5" y="0" width="2" height="8" fill="#a855f7" opacity="0.4">
<animate attributeName="height" values="8;14;8" dur="1.5s" repeatCount="indefinite"/>
</rect>
<rect x="10" y="0" width="2" height="12" fill="#ec4899" opacity="0.4">
<animate attributeName="height" values="10;16;10" dur="1s" repeatCount="indefinite"/>
</rect>
</g>
</g>
<!-- KG → SurrealDB -->
<line x1="320" y1="630" x2="420" y2="630" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-cyan" opacity="0.6"><animateMotion dur="1.5s" repeatCount="indefinite" path="M 320 630 L 420 630"/></use>
<!-- NATS JetStream -->
<g filter="url(#glass-card)">
<rect x="860" y="575" width="340" height="100" rx="8" fill="url(#grad-card-pink)" stroke="#ec4899" stroke-opacity="0.35" stroke-width="2"/>
<rect x="860" y="575" width="340" height="1.5" rx="1" fill="#ec4899" opacity="0.5"/>
<text x="1030" y="600" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="#ec4899" text-anchor="middle" filter="url(#glow-sm)">NATS JetStream</text>
<text x="1030" y="617" font-family="'Inter', sans-serif" font-size="9" fill="#ffffff" opacity="0.45" text-anchor="middle">Message Queue · Async Coordination · Pub/Sub</text>
<g transform="translate(875, 630)">
<rect x="0" y="0" width="75" height="18" rx="3" fill="rgba(236,72,153,0.1)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="37" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">Agent Jobs</text>
<rect x="85" y="0" width="75" height="18" rx="3" fill="rgba(34,211,238,0.1)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="122" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Workflows</text>
<rect x="170" y="0" width="70" height="18" rx="3" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="205" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Proposals</text>
<rect x="250" y="0" width="60" height="18" rx="3" fill="rgba(236,72,153,0.1)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="280" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">A2A</text>
</g>
<!-- Message pulse animation -->
<g transform="translate(1125, 590)">
<circle r="4" fill="none" stroke="#ffffff" stroke-width="2" opacity="0.07">
<animate attributeName="r" values="3;8;3" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;0.5;1" dur="2s" repeatCount="indefinite"/>
</circle>
<circle r="3" fill="#ffffff" opacity="0.07">
<animate attributeName="opacity" values="0.8;1;0.8" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
</g>
<!-- SurrealDB ↔ NATS bridge line -->
<line x1="760" y1="625" x2="860" y2="625" stroke="url(#grad-purple-pink)" stroke-width="0.8" stroke-opacity="0.4" stroke-dasharray="4 3"/>
<!-- ═══════════════════════════════════════ -->
<!-- CONNECTIONS: Data → Providers -->
<!-- ═══════════════════════════════════════ -->
<!-- Central provider trunk (from LLM Router, already drawn) -->
<!-- Branch to each provider -->
<path id="path-to-claude" d="M 790 725 Q 790 750 500 750 L 245 750 Q 225 750 225 785" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-cyan" opacity="0.6"><animateMotion dur="2.5s" repeatCount="indefinite"><mpath href="#path-to-claude"/></animateMotion></use>
<path id="path-to-openai" d="M 790 725 Q 790 760 600 760 L 555 760 Q 545 760 545 785" fill="none" stroke="#a855f7" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-purple" opacity="0.6"><animateMotion dur="2s" repeatCount="indefinite"><mpath href="#path-to-openai"/></animateMotion></use>
<path id="path-to-gemini" d="M 790 725 Q 790 770 810 770 L 855 770 Q 865 770 865 785" fill="none" stroke="#ec4899" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-pink" opacity="0.6"><animateMotion dur="2.2s" repeatCount="indefinite"><mpath href="#path-to-gemini"/></animateMotion></use>
<path id="path-to-ollama" d="M 790 725 Q 790 750 900 750 L 1155 750 Q 1175 750 1175 785" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-cyan" opacity="0.6"><animateMotion dur="2.8s" repeatCount="indefinite"><mpath href="#path-to-ollama"/></animateMotion></use>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 5: LLM PROVIDERS (y=810) -->
<!-- ═══════════════════════════════════════ -->
<!-- Claude -->
<g filter="url(#glass-card)">
<rect x="100" y="785" width="250" height="55" rx="6" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.8"/>
<text x="225" y="810" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#22d3ee" text-anchor="middle">Anthropic Claude</text>
<text x="225" y="827" font-family="'Inter', sans-serif" font-size="8" fill="#ffffff" opacity="0.4" text-anchor="middle">Opus · Sonnet · Haiku</text>
</g>
<!-- OpenAI -->
<g filter="url(#glass-card)">
<rect x="420" y="785" width="250" height="55" rx="6" fill="url(#grad-card-purple)" stroke="#a855f7" stroke-opacity="0.3" stroke-width="0.8"/>
<text x="545" y="810" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#a855f7" text-anchor="middle">OpenAI</text>
<text x="545" y="827" font-family="'Inter', sans-serif" font-size="8" fill="#ffffff" opacity="0.4" text-anchor="middle">GPT-4 · GPT-4o · GPT-3.5</text>
</g>
<!-- Gemini -->
<g filter="url(#glass-card)">
<rect x="740" y="785" width="250" height="55" rx="6" fill="url(#grad-card-pink)" stroke="#ec4899" stroke-opacity="0.3" stroke-width="0.8"/>
<text x="865" y="810" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#ec4899" text-anchor="middle">Google Gemini</text>
<text x="865" y="827" font-family="'Inter', sans-serif" font-size="8" fill="#ffffff" opacity="0.4" text-anchor="middle">2.0 Pro · Flash · 1.5 Pro</text>
</g>
<!-- Ollama -->
<g filter="url(#glass-card)">
<rect x="1060" y="785" width="250" height="55" rx="6" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.8"/>
<text x="1185" y="810" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#22d3ee" text-anchor="middle">Ollama (Local)</text>
<text x="1185" y="827" font-family="'Inter', sans-serif" font-size="8" fill="#ffffff" opacity="0.4" text-anchor="middle">Llama · Mistral · CodeLlama</text>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- SUPPORTING CRATES (bottom strip) -->
<!-- ═══════════════════════════════════════ -->
<g transform="translate(0, 882)">
<rect x="60" y="0" width="1280" height="50" rx="6" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="700" y="16" font-family="'JetBrains Mono', monospace" font-size="9" fill="#a855f7" opacity="0.7" text-anchor="middle" letter-spacing="2">SUPPORTING CRATES</text>
<g font-family="'Inter', sans-serif" font-size="10" opacity="0.6" text-anchor="middle">
<text x="130" y="36" fill="#22d3ee">vapora-shared</text>
<text x="260" y="36" fill="#a855f7">vapora-tracking</text>
<text x="400" y="36" fill="#ec4899">vapora-telemetry</text>
<text x="540" y="36" fill="#22d3ee">vapora-analytics</text>
<text x="680" y="36" fill="#a855f7">vapora-worktree</text>
<text x="820" y="36" fill="#ec4899">vapora-doc-lifecycle</text>
<text x="975" y="36" fill="#22d3ee">vapora-workflow-engine</text>
<text x="1140" y="36" fill="#a855f7">vapora-cli</text>
<text x="1250" y="36" fill="#ec4899">vapora-leptos-ui</text>
</g>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- OBSERVABILITY STRIP (right edge) -->
<!-- ═══════════════════════════════════════ -->
<g transform="translate(1340, 80)">
<rect x="0" y="0" width="4" height="790" rx="2" fill="url(#grad-vertical)" opacity="0.3">
<animate attributeName="opacity" values="0.2;0.4;0.2" dur="5s" repeatCount="indefinite"/>
</rect>
<text x="15" y="400" font-family="'JetBrains Mono', monospace" font-size="9" fill="#a855f7" opacity="0.6" letter-spacing="2" text-anchor="middle" transform="rotate(90 15 400)">PROMETHEUS · GRAFANA · OPENTELEMETRY</text>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- FOOTER -->
<!-- ═══════════════════════════════════════ -->
<text x="700" y="970" font-family="'JetBrains Mono', monospace" font-size="11" font-weight="700" fill="url(#grad-main)" text-anchor="middle" opacity="0.7" letter-spacing="3">VAPORA v1.2.0</text>
<text x="700" y="993" font-family="'Inter', sans-serif" font-size="9" fill="#a855f7" opacity="0.6" text-anchor="middle" letter-spacing="1">Evaporate complexity · Full-Stack Rust · Production Ready</text>
<!-- Bottom gradient bar -->
<rect x="400" y="1005" width="600" height="2" rx="1" fill="url(#grad-main)" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,576 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&amp;family=Inter:wght@400;500;600&amp;display=swap');
</style>
<!-- Brand gradients -->
<linearGradient id="grad-main" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#22d3ee"/>
<stop offset="50%" stop-color="#a855f7"/>
<stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
<linearGradient id="grad-cyan-purple" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#22d3ee"/>
<stop offset="100%" stop-color="#a855f7"/>
</linearGradient>
<linearGradient id="grad-purple-pink" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#a855f7"/>
<stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
<linearGradient id="grad-vertical" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.8"/>
<stop offset="50%" stop-color="#a855f7" stop-opacity="0.5"/>
<stop offset="100%" stop-color="#ec4899" stop-opacity="0.3"/>
</linearGradient>
<linearGradient id="grad-card-cyan" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.12"/>
<stop offset="100%" stop-color="#22d3ee" stop-opacity="0.03"/>
</linearGradient>
<linearGradient id="grad-card-purple" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#a855f7" stop-opacity="0.12"/>
<stop offset="100%" stop-color="#a855f7" stop-opacity="0.03"/>
</linearGradient>
<linearGradient id="grad-card-pink" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ec4899" stop-opacity="0.12"/>
<stop offset="100%" stop-color="#ec4899" stop-opacity="0.03"/>
</linearGradient>
<!-- Glow filters -->
<filter id="glow-sm">
<feGaussianBlur stdDeviation="0.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-md">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-lg">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glass-card">
<feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur"/>
<feOffset dx="0" dy="1" result="shadow"/>
<feFlood flood-color="#000" flood-opacity="0.3" result="color"/>
<feComposite in="color" in2="shadow" operator="in" result="shadow"/>
<feMerge><feMergeNode in="shadow"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- Animated particle marker -->
<circle id="particle-cyan" r="3" fill="#22d3ee" filter="url(#glow-md)"/>
<circle id="particle-purple" r="3" fill="#a855f7" filter="url(#glow-md)"/>
<circle id="particle-pink" r="3" fill="#ec4899" filter="url(#glow-md)"/>
<circle id="particle-sm-cyan" r="2" fill="#22d3ee" filter="url(#glow-sm)"/>
<circle id="particle-sm-purple" r="2" fill="#a855f7" filter="url(#glow-sm)"/>
<circle id="particle-sm-pink" r="2" fill="#ec4899" filter="url(#glow-sm)"/>
</defs>
<!-- ═══════════════════════════════════════ -->
<!-- BACKGROUND -->
<!-- ═══════════════════════════════════════ -->
<rect width="1400" height="1020" fill="#ffffff"/>
<!-- Subtle tech grid -->
<g opacity="0.08" stroke="#22d3ee" stroke-width="0.5">
<line x1="0" y1="0" x2="1400" y2="0"><animate attributeName="y1" values="0;1020" dur="60s" repeatCount="indefinite"/><animate attributeName="y2" values="0;1020" dur="60s" repeatCount="indefinite"/></line>
<line x1="100" y1="0" x2="100" y2="1020"/>
<line x1="250" y1="0" x2="250" y2="1020"/>
<line x1="400" y1="0" x2="400" y2="1020"/>
<line x1="550" y1="0" x2="550" y2="1020"/>
<line x1="700" y1="0" x2="700" y2="1020"/>
<line x1="850" y1="0" x2="850" y2="1020"/>
<line x1="1000" y1="0" x2="1000" y2="1020"/>
<line x1="1150" y1="0" x2="1150" y2="1020"/>
<line x1="1300" y1="0" x2="1300" y2="1020"/>
<line x1="0" y1="130" x2="1400" y2="130"/>
<line x1="0" y1="260" x2="1400" y2="260"/>
<line x1="0" y1="420" x2="1400" y2="420"/>
<line x1="0" y1="580" x2="1400" y2="580"/>
<line x1="0" y1="720" x2="1400" y2="720"/>
<line x1="0" y1="860" x2="1400" y2="860"/>
</g>
<!-- Ambient glow orbs -->
<circle cx="240" cy="350" r="180" fill="#22d3ee" opacity="0.05">
<animate attributeName="r" values="160;200;160" dur="8s" repeatCount="indefinite"/>
</circle>
<circle cx="690" cy="360" r="175" fill="#a855f7" opacity="0.04">
<animate attributeName="r" values="175;195;175" dur="10s" repeatCount="indefinite"/>
</circle>
<circle cx="1130" cy="350" r="200" fill="#ec4899" opacity="0.05">
<animate attributeName="r" values="180;220;180" dur="9s" repeatCount="indefinite"/>
</circle>
<!-- ═══════════════════════════════════════ -->
<!-- TITLE -->
<!-- ═══════════════════════════════════════ -->
<text x="700" y="42" font-family="'JetBrains Mono', monospace" font-size="22" font-weight="800" fill="url(#grad-main)" letter-spacing="6" text-anchor="middle" filter="url(#glow-sm)">VAPORA ARCHITECTURE</text>
<text x="700" y="62" font-family="'Inter', sans-serif" font-size="11" fill="#a855f7" opacity="0.6" letter-spacing="3" text-anchor="middle">18 CRATES · 354 TESTS · 100% RUST</text>
<!-- Layer labels (left side) -->
<text x="30" y="115" font-family="'JetBrains Mono', monospace" font-size="10" fill="#22d3ee" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 115)">PRESENTATION</text>
<text x="30" y="290" font-family="'JetBrains Mono', monospace" font-size="10" fill="#a855f7" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 290)">SERVICES</text>
<text x="30" y="485" font-family="'JetBrains Mono', monospace" font-size="10" fill="#ec4899" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 485)">INTELLIGENCE</text>
<text x="30" y="640" font-family="'JetBrains Mono', monospace" font-size="10" fill="#22d3ee" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 640)">DATA</text>
<text x="30" y="850" font-family="'JetBrains Mono', monospace" font-size="10" fill="#a855f7" opacity="0.7" letter-spacing="2" transform="rotate(-90 30 850)">PROVIDERS</text>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 1: FRONTEND (y=80) -->
<!-- ═══════════════════════════════════════ -->
<!-- Frontend card -->
<g filter="url(#glass-card)">
<rect x="490" y="80" width="420" height="65" rx="8" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.4" stroke-width="1"/>
<rect x="494" y="80" width="412" height="1.5" rx="1" fill="url(#grad-main)" opacity="0.8"/>
<text x="700" y="106" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="#22d3ee" text-anchor="middle" filter="url(#glow-sm)">Leptos WASM Frontend</text>
<text x="700" y="124" font-family="'Inter', sans-serif" font-size="10" fill="#1f2937" opacity="0.85" text-anchor="middle">Kanban Board · Glassmorphism UI · UnoCSS · Reactive Components</text>
<!-- Pulse ring -->
<rect x="490" y="80" width="420" height="65" rx="8" fill="none" stroke="#22d3ee" stroke-opacity="0.15">
<animate attributeName="stroke-opacity" values="0.15;0.3;0.15" dur="3s" repeatCount="indefinite"/>
</rect>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- CONNECTION: Frontend → Services -->
<!-- ═══════════════════════════════════════ -->
<!-- Frontend to Backend -->
<path id="path-fe-be" d="M 580 145 L 580 160 Q 580 175 480 175 L 280 175 Q 260 175 260 195 L 260 210" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-cyan" opacity="0.8"><animateMotion dur="2.5s" repeatCount="indefinite"><mpath href="#path-fe-be"/></animateMotion></use>
<!-- Frontend to Agents -->
<line x1="700" y1="145" x2="700" y2="210" stroke="#a855f7" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-purple" opacity="0.8"><animateMotion dur="2s" repeatCount="indefinite" path="M 700 145 L 700 210"/></use>
<!-- Frontend to MCP -->
<path id="path-fe-mcp" d="M 820 145 L 820 160 Q 820 175 920 175 L 1100 175 Q 1140 175 1140 195 L 1140 210" fill="none" stroke="#ec4899" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-pink" opacity="0.8"><animateMotion dur="2.5s" repeatCount="indefinite"><mpath href="#path-fe-mcp"/></animateMotion></use>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 2: SERVICES (y=210) -->
<!-- ═══════════════════════════════════════ -->
<!-- Backend / Axum API -->
<g filter="url(#glass-card)">
<rect x="100" y="210" width="310" height="100" rx="8" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.35" stroke-width="1"/>
<rect x="104" y="210" width="302" height="1.5" rx="1" fill="#22d3ee" opacity="0.6"/>
<text x="255" y="237" font-family="'JetBrains Mono', monospace" font-size="13" font-weight="700" fill="#22d3ee" text-anchor="middle">Axum Backend API</text>
<text x="255" y="256" font-family="'Inter', sans-serif" font-size="9" fill="#1f2937" opacity="0.75" text-anchor="middle">40+ REST Endpoints · 161 Tests</text>
<!-- Sub-items -->
<g font-family="'Inter', sans-serif" font-size="8" fill="#1f2937" opacity="0.65">
<text x="120" y="278">Projects</text>
<text x="180" y="278">Tasks</text>
<text x="225" y="278">Agents</text>
<text x="280" y="278">Workflows</text>
<text x="350" y="278">Proposals</text>
<text x="120" y="294">Swarm</text>
<text x="175" y="294">Metrics</text>
<text x="235" y="294">RLM API</text>
<text x="300" y="294">WebSocket</text>
</g>
</g>
<!-- Agent Runtime -->
<g filter="url(#glass-card)">
<rect x="540" y="210" width="310" height="100" rx="8" fill="url(#grad-card-purple)" stroke="#a855f7" stroke-opacity="0.35" stroke-width="1"/>
<rect x="544" y="210" width="302" height="1.5" rx="1" fill="#a855f7" opacity="0.6"/>
<text x="695" y="237" font-family="'JetBrains Mono', monospace" font-size="13" font-weight="700" fill="#a855f7" text-anchor="middle">Agent Runtime</text>
<text x="695" y="256" font-family="'Inter', sans-serif" font-size="9" fill="#1f2937" opacity="0.75" text-anchor="middle">Orchestration · Learning Profiles · 71 Tests</text>
<g font-family="'Inter', sans-serif" font-size="8" fill="#1f2937" opacity="0.65">
<text x="560" y="278">Registry</text>
<text x="625" y="278">Coordinator</text>
<text x="720" y="278">Scoring</text>
<text x="785" y="278">Profiles</text>
<text x="560" y="294">12 Roles</text>
<text x="630" y="294">Health Checks</text>
<text x="735" y="294">Recency Bias</text>
</g>
<!-- Heartbeat animation -->
<circle cx="835" cy="225" r="4" fill="#a855f7" opacity="0.04">
<animate attributeName="r" values="3;5;3" dur="1.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.3;0.6;0.3" dur="1.5s" repeatCount="indefinite"/>
</circle>
</g>
<!-- MCP Gateway -->
<g filter="url(#glass-card)">
<rect x="980" y="210" width="310" height="100" rx="8" fill="url(#grad-card-pink)" stroke="#ec4899" stroke-opacity="0.35" stroke-width="1"/>
<rect x="984" y="210" width="302" height="1.5" rx="1" fill="#ec4899" opacity="0.6"/>
<text x="1135" y="237" font-family="'JetBrains Mono', monospace" font-size="13" font-weight="700" fill="#ec4899" text-anchor="middle">MCP Gateway</text>
<text x="1135" y="256" font-family="'Inter', sans-serif" font-size="9" fill="#1f2937" opacity="0.75" text-anchor="middle">Model Context Protocol · Plugin System</text>
<g font-family="'Inter', sans-serif" font-size="8" fill="#1f2937" opacity="0.65">
<text x="1000" y="278">Stdio Transport</text>
<text x="1105" y="278">SSE Transport</text>
<text x="1210" y="278">JSON-RPC</text>
<text x="1000" y="294">6 Tools</text>
<text x="1060" y="294">Tool Registry</text>
<text x="1160" y="294">Schema Validation</text>
</g>
</g>
<!-- A2A Protocol (small card overlapping) -->
<g filter="url(#glass-card)">
<rect x="445" y="280" width="86" height="35" rx="6" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="488" y="300" font-family="'JetBrains Mono', monospace" font-size="9" font-weight="700" fill="#a855f7" opacity="0.6" text-anchor="middle">A2A Protocol</text>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- CONNECTIONS: Services → Intelligence -->
<!-- ═══════════════════════════════════════ -->
<!-- Backend → RLM -->
<path id="path-be-rlm" d="M 200 310 L 200 380" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-cyan" opacity="0.7"><animateMotion dur="1.8s" repeatCount="indefinite" path="M 200 310 L 200 380"/></use>
<!-- Backend → SurrealDB (arco por la derecha para evitar KG) -->
<path id="path-be-sdb" d="M 320 310 L 320 340 Q 320 360 360 360 L 420 360 Q 435 360 435 380 Q 435 580 420 580 L 420 585" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.35" stroke-dasharray="4 4"/>
<!-- Agents → LLM Router -->
<line x1="695" y1="310" x2="695" y2="380" stroke="#a855f7" stroke-width="2.4" stroke-opacity="0.3"/>
<use href="#particle-purple" opacity="0.8"><animateMotion dur="1.5s" repeatCount="indefinite" path="M 695 310 L 695 380"/></use>
<use href="#particle-sm-purple" opacity="0.5"><animateMotion dur="1.5s" begin="0.75s" repeatCount="indefinite" path="M 695 310 L 695 380"/></use>
<!-- Agents → Swarm -->
<path id="path-ag-sw" d="M 800 310 L 800 340 Q 800 355 900 355 L 1050 355 Q 1070 355 1070 375 L 1070 380" fill="none" stroke="#a855f7" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-purple" opacity="0.6"><animateMotion dur="2.2s" repeatCount="indefinite"><mpath href="#path-ag-sw"/></animateMotion></use>
<!-- MCP → Agents -->
<path id="path-mcp-ag" d="M 1020 310 L 1020 320 Q 1020 332 920 332 L 810 332 Q 800 332 800 320 L 800 310" fill="none" stroke="#ec4899" stroke-width="1.6" stroke-opacity="0.35" stroke-dasharray="3 3"/>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 3: INTELLIGENCE (y=380) -->
<!-- ═══════════════════════════════════════ -->
<!-- RLM Engine -->
<g filter="url(#glass-card)">
<rect x="60" y="380" width="340" height="130" rx="8" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.4" stroke-width="1.2"/>
<rect x="64" y="380" width="332" height="2" rx="1" fill="url(#grad-main)" opacity="0.7"/>
<text x="230" y="405" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="url(#grad-main)" text-anchor="middle" filter="url(#glow-sm)">RLM Engine</text>
<text x="230" y="422" font-family="'Inter', sans-serif" font-size="9" fill="#1f2937" opacity="0.75" text-anchor="middle">Recursive Language Models · 38 Tests · 17k+ LOC</text>
<!-- RLM sub-components as mini pills -->
<g transform="translate(75, 435)">
<!-- Chunking -->
<rect x="0" y="0" width="80" height="22" rx="4" fill="rgba(34,211,238,0.12)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="40" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#22d3ee" opacity="0.8" text-anchor="middle" dominant-baseline="central">Chunking</text>
<!-- Hybrid Search -->
<rect x="90" y="0" width="95" height="22" rx="4" fill="rgba(168,85,247,0.12)" stroke="#a855f7" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="137" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#a855f7" opacity="0.8" text-anchor="middle" dominant-baseline="central">Hybrid Search</text>
<!-- Dispatcher -->
<rect x="195" y="0" width="80" height="22" rx="4" fill="rgba(236,72,153,0.12)" stroke="#ec4899" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="235" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#ec4899" opacity="0.8" text-anchor="middle" dominant-baseline="central">Dispatcher</text>
</g>
<g transform="translate(75, 465)">
<!-- BM25 -->
<rect x="0" y="0" width="65" height="20" rx="3" fill="rgba(34,211,238,0.08)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="32" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">BM25</text>
<!-- Semantic -->
<rect x="75" y="0" width="70" height="20" rx="3" fill="rgba(168,85,247,0.08)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="110" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Semantic</text>
<!-- RRF -->
<rect x="155" y="0" width="50" height="20" rx="3" fill="rgba(236,72,153,0.08)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="180" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">RRF</text>
<!-- Sandbox -->
<rect x="215" y="0" width="60" height="20" rx="3" fill="rgba(34,211,238,0.08)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="245" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Sandbox</text>
</g>
<!-- Animated search indicator -->
<g transform="translate(370, 400)">
<circle r="6" fill="none" stroke="#22d3ee" stroke-width="0.8" opacity="0.07">
<animate attributeName="r" values="4;8;4" dur="3s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.4;0.1;0.4" dur="3s" repeatCount="indefinite"/>
</circle>
<circle r="3" fill="#22d3ee" opacity="0.07">
<animate attributeName="opacity" values="0.2;0.5;0.2" dur="3s" repeatCount="indefinite"/>
</circle>
</g>
</g>
<!-- LLM Router -->
<g filter="url(#glass-card)">
<rect x="500" y="380" width="380" height="130" rx="8" fill="url(#grad-card-purple)" stroke="#a855f7" stroke-opacity="0.4" stroke-width="1.2"/>
<rect x="504" y="380" width="372" height="2" rx="1" fill="#a855f7" opacity="0.7"/>
<text x="690" y="405" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="#a855f7" text-anchor="middle" filter="url(#glow-sm)">Multi-IA LLM Router</text>
<text x="690" y="422" font-family="'Inter', sans-serif" font-size="9" fill="#1f2937" opacity="0.75" text-anchor="middle">Budget Enforcement · Cost Tracking · 53 Tests</text>
<!-- Router sub-components -->
<g transform="translate(515, 435)">
<rect x="0" y="0" width="95" height="22" rx="4" fill="rgba(168,85,247,0.12)" stroke="#a855f7" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="47" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#a855f7" opacity="0.8" text-anchor="middle" dominant-baseline="central">Rule Router</text>
<rect x="105" y="0" width="115" height="22" rx="4" fill="rgba(236,72,153,0.12)" stroke="#ec4899" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="162" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#ec4899" opacity="0.8" text-anchor="middle" dominant-baseline="central">Budget Manager</text>
<rect x="230" y="0" width="110" height="22" rx="4" fill="rgba(34,211,238,0.12)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="285" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#22d3ee" opacity="0.8" text-anchor="middle" dominant-baseline="central">Cost Tracker</text>
</g>
<g transform="translate(515, 465)">
<rect x="0" y="0" width="95" height="20" rx="3" fill="rgba(168,85,247,0.08)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="47" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Fallback Chain</text>
<rect x="105" y="0" width="90" height="20" rx="3" fill="rgba(236,72,153,0.08)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="150" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">Cost Ranker</text>
<rect x="205" y="0" width="80" height="20" rx="3" fill="rgba(34,211,238,0.08)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="245" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Providers</text>
</g>
<!-- Routing indicator animation -->
<g transform="translate(855, 400)">
<line x1="-6" y1="0" x2="6" y2="0" stroke="#a855f7" stroke-width="1.5" opacity="0.5">
<animateTransform attributeName="transform" type="rotate" values="0;360" dur="4s" repeatCount="indefinite"/>
</line>
<line x1="0" y1="-6" x2="0" y2="6" stroke="#ec4899" stroke-width="1.5" opacity="0.5">
<animateTransform attributeName="transform" type="rotate" values="0;360" dur="4s" repeatCount="indefinite"/>
</line>
</g>
</g>
<!-- Swarm Coordinator -->
<g filter="url(#glass-card)">
<rect x="980" y="380" width="310" height="130" rx="8" fill="url(#grad-card-pink)" stroke="#ec4899" stroke-opacity="0.35" stroke-width="1"/>
<rect x="984" y="380" width="302" height="1.5" rx="1" fill="#ec4899" opacity="0.6"/>
<text x="1135" y="405" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="#ec4899" text-anchor="middle" filter="url(#glow-sm)">Swarm Coordinator</text>
<text x="1135" y="422" font-family="'Inter', sans-serif" font-size="9" fill="#1f2937" opacity="0.75" text-anchor="middle">Load Balancing · Prometheus · 6 Tests</text>
<g transform="translate(995, 435)">
<rect x="0" y="0" width="90" height="22" rx="4" fill="rgba(236,72,153,0.12)" stroke="#ec4899" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="45" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#ec4899" opacity="0.8" text-anchor="middle" dominant-baseline="central">Assignment</text>
<rect x="100" y="0" width="90" height="22" rx="4" fill="rgba(34,211,238,0.12)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="145" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#22d3ee" opacity="0.8" text-anchor="middle" dominant-baseline="central">Filtering</text>
<rect x="200" y="0" width="80" height="22" rx="4" fill="rgba(168,85,247,0.12)" stroke="#a855f7" stroke-opacity="0.3" stroke-width="0.5"/>
<text x="240" y="11" font-family="'JetBrains Mono', monospace" font-size="7" fill="#a855f7" opacity="0.8" text-anchor="middle" dominant-baseline="central">Metrics</text>
</g>
<g transform="translate(995, 465)">
<rect x="0" y="0" width="130" height="20" rx="3" fill="rgba(236,72,153,0.08)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="65" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">success_rate / (1+load)</text>
<rect x="140" y="0" width="80" height="20" rx="3" fill="rgba(34,211,238,0.08)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="180" y="10" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Capabilities</text>
</g>
<!-- Swarm pulse -->
<circle cx="1270" cy="395" r="3" fill="#ec4899" opacity="0.04">
<animate attributeName="r" values="2;5;2" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.2;0.5;0.2" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- RLM → LLM Router connection -->
<path id="path-rlm-llm" d="M 400 445 L 500 445" fill="none" stroke="url(#grad-cyan-purple)" stroke-width="3" stroke-opacity="0.3"/>
<use href="#particle-cyan" opacity="0.7"><animateMotion dur="1.5s" repeatCount="indefinite" path="M 400 445 L 500 445"/></use>
<!-- LLM Router → Swarm connection -->
<path d="M 880 445 L 980 445" fill="none" stroke="url(#grad-purple-pink)" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-pink" opacity="0.6"><animateMotion dur="1.8s" repeatCount="indefinite" path="M 880 445 L 980 445"/></use>
<!-- ═══════════════════════════════════════ -->
<!-- CONNECTIONS: Intelligence → Data -->
<!-- ═══════════════════════════════════════ -->
<!-- RLM → KG -->
<path id="path-rlm-kg" d="M 160 510 L 160 575" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-cyan" opacity="0.7"><animateMotion dur="1.8s" repeatCount="indefinite" path="M 160 510 L 160 575"/></use>
<!-- RLM → SurrealDB -->
<path id="path-rlm-sdb" d="M 300 510 L 300 540 Q 300 555 400 555 L 530 555 Q 545 555 545 575" fill="none" stroke="#22d3ee" stroke-width="2.4" stroke-opacity="0.25"/>
<use href="#particle-cyan" opacity="0.7"><animateMotion dur="2.5s" repeatCount="indefinite"><mpath href="#path-rlm-sdb"/></animateMotion></use>
<!-- LLM Router → Providers (goes down below data) -->
<path id="path-llm-providers" d="M 790 510 L 790 725" fill="none" stroke="#a855f7" stroke-width="3" stroke-opacity="0.2"/>
<use href="#particle-purple" opacity="0.8"><animateMotion dur="3s" repeatCount="indefinite" path="M 790 510 L 790 725"/></use>
<use href="#particle-sm-purple" opacity="0.5"><animateMotion dur="3s" begin="1.5s" repeatCount="indefinite" path="M 790 510 L 790 725"/></use>
<!-- Swarm → NATS -->
<path id="path-sw-nats" d="M 1135 510 L 1135 575" fill="none" stroke="#ec4899" stroke-width="2" stroke-opacity="0.25"/>
<use href="#particle-sm-pink" opacity="0.7"><animateMotion dur="1.8s" repeatCount="indefinite" path="M 1135 510 L 1135 575"/></use>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 4: DATA LAYER (y=575) -->
<!-- ═══════════════════════════════════════ -->
<!-- Knowledge Graph -->
<g filter="url(#glass-card)">
<rect x="60" y="575" width="260" height="100" rx="8" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="1"/>
<rect x="60" y="575" width="260" height="1.5" rx="1" fill="#22d3ee" opacity="0.5"/>
<text x="190" y="600" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#22d3ee" text-anchor="middle">Knowledge Graph</text>
<text x="190" y="617" font-family="'Inter', sans-serif" font-size="9" fill="#1f2937" opacity="0.7" text-anchor="middle">Temporal History · Learning Curves · 20 Tests</text>
<g transform="translate(75, 630)">
<rect x="0" y="0" width="80" height="18" rx="3" fill="rgba(34,211,238,0.1)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="40" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Executions</text>
<rect x="90" y="0" width="70" height="18" rx="3" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="125" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Similarity</text>
<rect x="170" y="0" width="60" height="18" rx="3" fill="rgba(236,72,153,0.1)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="200" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">Causal</text>
</g>
</g>
<!-- SurrealDB -->
<g filter="url(#glass-card)">
<rect x="420" y="575" width="340" height="100" rx="8" fill="url(#grad-card-purple)" stroke="#a855f7" stroke-opacity="0.4" stroke-width="1.2"/>
<rect x="420" y="575" width="340" height="2" rx="1" fill="url(#grad-main)" opacity="0.6"/>
<text x="590" y="600" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="url(#grad-main)" text-anchor="middle" filter="url(#glow-sm)">SurrealDB</text>
<text x="590" y="617" font-family="'Inter', sans-serif" font-size="9" fill="#1f2937" opacity="0.75" text-anchor="middle">Multi-Model Database · Multi-Tenant Scopes</text>
<g transform="translate(435, 630)">
<rect x="0" y="0" width="65" height="18" rx="3" fill="rgba(34,211,238,0.1)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="32" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Projects</text>
<rect x="75" y="0" width="50" height="18" rx="3" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="100" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Tasks</text>
<rect x="135" y="0" width="55" height="18" rx="3" fill="rgba(236,72,153,0.1)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="162" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">Agents</text>
<rect x="200" y="0" width="55" height="18" rx="3" fill="rgba(34,211,238,0.1)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="227" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Chunks</text>
<rect x="265" y="0" width="45" height="18" rx="3" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="287" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">A2A</text>
</g>
<!-- DB activity indicator -->
<g transform="translate(735, 590)">
<rect x="0" y="0" width="2" height="10" fill="#22d3ee" opacity="0.4">
<animate attributeName="height" values="6;12;6" dur="1.2s" repeatCount="indefinite"/>
</rect>
<rect x="5" y="0" width="2" height="8" fill="#a855f7" opacity="0.4">
<animate attributeName="height" values="8;14;8" dur="1.5s" repeatCount="indefinite"/>
</rect>
<rect x="10" y="0" width="2" height="12" fill="#ec4899" opacity="0.4">
<animate attributeName="height" values="10;16;10" dur="1s" repeatCount="indefinite"/>
</rect>
</g>
</g>
<!-- KG → SurrealDB -->
<line x1="320" y1="630" x2="420" y2="630" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-cyan" opacity="0.6"><animateMotion dur="1.5s" repeatCount="indefinite" path="M 320 630 L 420 630"/></use>
<!-- NATS JetStream -->
<g filter="url(#glass-card)">
<rect x="860" y="575" width="340" height="100" rx="8" fill="url(#grad-card-pink)" stroke="#ec4899" stroke-opacity="0.35" stroke-width="1"/>
<rect x="860" y="575" width="340" height="1.5" rx="1" fill="#ec4899" opacity="0.5"/>
<text x="1030" y="600" font-family="'JetBrains Mono', monospace" font-size="14" font-weight="700" fill="#ec4899" text-anchor="middle" filter="url(#glow-sm)">NATS JetStream</text>
<text x="1030" y="617" font-family="'Inter', sans-serif" font-size="9" fill="#1f2937" opacity="0.75" text-anchor="middle">Message Queue · Async Coordination · Pub/Sub</text>
<g transform="translate(875, 630)">
<rect x="0" y="0" width="75" height="18" rx="3" fill="rgba(236,72,153,0.1)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="37" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">Agent Jobs</text>
<rect x="85" y="0" width="75" height="18" rx="3" fill="rgba(34,211,238,0.1)" stroke="#22d3ee" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="122" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#22d3ee" opacity="0.6" text-anchor="middle" dominant-baseline="central">Workflows</text>
<rect x="170" y="0" width="70" height="18" rx="3" fill="rgba(168,85,247,0.1)" stroke="#a855f7" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="205" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#a855f7" opacity="0.6" text-anchor="middle" dominant-baseline="central">Proposals</text>
<rect x="250" y="0" width="60" height="18" rx="3" fill="rgba(236,72,153,0.1)" stroke="#ec4899" stroke-opacity="0.2" stroke-width="0.5"/>
<text x="280" y="9" font-family="'Inter', sans-serif" font-size="7" fill="#ec4899" opacity="0.6" text-anchor="middle" dominant-baseline="central">A2A</text>
</g>
<!-- Message pulse animation -->
<g transform="translate(1125, 590)">
<circle r="4" fill="none" stroke="#22d3ee" stroke-width="1.5" opacity="0.07">
<animate attributeName="r" values="3;8;3" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>
</circle>
<circle r="2" fill="#22d3ee" opacity="0.07">
<animate attributeName="opacity" values="0.7;1;0.7" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
</g>
<!-- SurrealDB ↔ NATS bridge line -->
<line x1="760" y1="625" x2="860" y2="625" stroke="url(#grad-purple-pink)" stroke-width="0.8" stroke-opacity="0.4" stroke-dasharray="4 3"/>
<!-- ═══════════════════════════════════════ -->
<!-- CONNECTIONS: Data → Providers -->
<!-- ═══════════════════════════════════════ -->
<!-- Central provider trunk (from LLM Router, already drawn) -->
<!-- Branch to each provider -->
<path id="path-to-claude" d="M 790 725 Q 790 750 500 750 L 245 750 Q 225 750 225 785" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-cyan" opacity="0.6"><animateMotion dur="2.5s" repeatCount="indefinite"><mpath href="#path-to-claude"/></animateMotion></use>
<path id="path-to-openai" d="M 790 725 Q 790 760 600 760 L 555 760 Q 545 760 545 785" fill="none" stroke="#a855f7" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-purple" opacity="0.6"><animateMotion dur="2s" repeatCount="indefinite"><mpath href="#path-to-openai"/></animateMotion></use>
<path id="path-to-gemini" d="M 790 725 Q 790 770 810 770 L 855 770 Q 865 770 865 785" fill="none" stroke="#ec4899" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-pink" opacity="0.6"><animateMotion dur="2.2s" repeatCount="indefinite"><mpath href="#path-to-gemini"/></animateMotion></use>
<path id="path-to-ollama" d="M 790 725 Q 790 750 900 750 L 1155 750 Q 1175 750 1175 785" fill="none" stroke="#22d3ee" stroke-width="2" stroke-opacity="0.2"/>
<use href="#particle-sm-cyan" opacity="0.6"><animateMotion dur="2.8s" repeatCount="indefinite"><mpath href="#path-to-ollama"/></animateMotion></use>
<!-- ═══════════════════════════════════════ -->
<!-- ROW 5: LLM PROVIDERS (y=810) -->
<!-- ═══════════════════════════════════════ -->
<!-- Claude -->
<g filter="url(#glass-card)">
<rect x="100" y="785" width="250" height="55" rx="6" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.8"/>
<text x="225" y="810" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#22d3ee" text-anchor="middle">Anthropic Claude</text>
<text x="225" y="827" font-family="'Inter', sans-serif" font-size="8" fill="#1f2937" opacity="0.7" text-anchor="middle">Opus · Sonnet · Haiku</text>
</g>
<!-- OpenAI -->
<g filter="url(#glass-card)">
<rect x="420" y="785" width="250" height="55" rx="6" fill="url(#grad-card-purple)" stroke="#a855f7" stroke-opacity="0.3" stroke-width="0.8"/>
<text x="545" y="810" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#a855f7" text-anchor="middle">OpenAI</text>
<text x="545" y="827" font-family="'Inter', sans-serif" font-size="8" fill="#1f2937" opacity="0.7" text-anchor="middle">GPT-4 · GPT-4o · GPT-3.5</text>
</g>
<!-- Gemini -->
<g filter="url(#glass-card)">
<rect x="740" y="785" width="250" height="55" rx="6" fill="url(#grad-card-pink)" stroke="#ec4899" stroke-opacity="0.3" stroke-width="0.8"/>
<text x="865" y="810" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#ec4899" text-anchor="middle">Google Gemini</text>
<text x="865" y="827" font-family="'Inter', sans-serif" font-size="8" fill="#1f2937" opacity="0.7" text-anchor="middle">2.0 Pro · Flash · 1.5 Pro</text>
</g>
<!-- Ollama -->
<g filter="url(#glass-card)">
<rect x="1060" y="785" width="250" height="55" rx="6" fill="url(#grad-card-cyan)" stroke="#22d3ee" stroke-opacity="0.3" stroke-width="0.8"/>
<text x="1185" y="810" font-family="'JetBrains Mono', monospace" font-size="12" font-weight="700" fill="#22d3ee" text-anchor="middle">Ollama (Local)</text>
<text x="1185" y="827" font-family="'Inter', sans-serif" font-size="8" fill="#1f2937" opacity="0.7" text-anchor="middle">Llama · Mistral · CodeLlama</text>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- SUPPORTING CRATES (bottom strip) -->
<!-- ═══════════════════════════════════════ -->
<g transform="translate(0, 882)">
<rect x="60" y="0" width="1280" height="50" rx="6" fill="rgba(168,85,247,0.04)" stroke="#a855f7" stroke-opacity="0.1" stroke-width="0.5"/>
<text x="700" y="16" font-family="'JetBrains Mono', monospace" font-size="9" fill="#a855f7" opacity="0.7" text-anchor="middle" letter-spacing="2">SUPPORTING CRATES</text>
<g font-family="'Inter', sans-serif" font-size="10" opacity="0.6" text-anchor="middle">
<text x="130" y="36" fill="#22d3ee">vapora-shared</text>
<text x="260" y="36" fill="#a855f7">vapora-tracking</text>
<text x="400" y="36" fill="#ec4899">vapora-telemetry</text>
<text x="540" y="36" fill="#22d3ee">vapora-analytics</text>
<text x="680" y="36" fill="#a855f7">vapora-worktree</text>
<text x="820" y="36" fill="#ec4899">vapora-doc-lifecycle</text>
<text x="975" y="36" fill="#22d3ee">vapora-workflow-engine</text>
<text x="1140" y="36" fill="#a855f7">vapora-cli</text>
<text x="1250" y="36" fill="#ec4899">vapora-leptos-ui</text>
</g>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- OBSERVABILITY STRIP (right edge) -->
<!-- ═══════════════════════════════════════ -->
<g transform="translate(1340, 80)">
<rect x="0" y="0" width="4" height="790" rx="2" fill="url(#grad-vertical)" opacity="0.3">
<animate attributeName="opacity" values="0.2;0.4;0.2" dur="5s" repeatCount="indefinite"/>
</rect>
<text x="15" y="400" font-family="'JetBrains Mono', monospace" font-size="9" fill="#a855f7" opacity="0.6" letter-spacing="2" text-anchor="middle" transform="rotate(90 15 400)">PROMETHEUS · GRAFANA · OPENTELEMETRY</text>
</g>
<!-- ═══════════════════════════════════════ -->
<!-- FOOTER -->
<!-- ═══════════════════════════════════════ -->
<text x="700" y="970" font-family="'JetBrains Mono', monospace" font-size="11" font-weight="700" fill="url(#grad-main)" text-anchor="middle" opacity="0.7" letter-spacing="3">VAPORA v1.2.0</text>
<text x="700" y="993" font-family="'Inter', sans-serif" font-size="9" fill="#a855f7" opacity="0.6" text-anchor="middle" letter-spacing="1">Evaporate complexity · Full-Stack Rust · Production Ready</text>
<!-- Bottom gradient bar -->
<rect x="400" y="1005" width="600" height="2" rx="1" fill="url(#grad-main)" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,262 @@
/**
* content-graph-shim.js
*
* Thin bridge between Leptos/WASM and Cytoscape.js.
* Exposes window.ContentGraph with a single render() entry point.
*
* Called from GraphView component after mount:
* window.ContentGraph.render(containerId, focusNodeId, theme)
*
* Reads graph data from:
* <script type="application/json" id="content-graph-data">{ nodes, edges }</script>
*
* Requires (loaded before this script via assets pipeline):
* - cytoscape.min.js
* - cytoscape-cose-bilkent.min.js
*/
(function () {
'use strict';
// ── Theme tokens ────────────────────────────────────────────────────────────
function buildPalette(isDark) {
return {
// Content node variants
contentFill: isDark ? '#3b82f6' : '#2563eb',
// Ontology node variants by level
axiomFill: isDark ? '#8b5cf6' : '#7c3aed',
tensionFill: isDark ? '#f59e0b' : '#d97706',
practiceFill: isDark ? '#10b981' : '#059669',
projectFill: isDark ? '#06b6d4' : '#0891b2',
// ADR
adrFill: isDark ? '#f97316' : '#ea580c',
// Ego node ring
egoStroke: isDark ? '#facc15' : '#ca8a04',
// Edge colours by kind group
declaredEdge: isDark ? '#6b7280' : '#9ca3af',
autoEdge: isDark ? '#4b5563' : '#d1d5db',
implicitEdge: isDark ? '#374151' : '#e5e7eb',
// Labels
labelColor: isDark ? '#f9fafb' : '#111827',
labelBg: isDark ? 'rgba(17,24,39,0.75)' : 'rgba(255,255,255,0.75)',
// Background
bg: isDark ? '#111827' : '#ffffff',
};
}
// ── Cytoscape stylesheet ─────────────────────────────────────────────────────
function buildStylesheet(palette, egoId) {
const label = {
label: 'data(title)',
color: palette.labelColor,
'background-color':palette.labelBg,
'background-opacity': 0.75,
'background-padding': '3px',
'font-size': '11px',
'text-valign': 'bottom',
'text-halign': 'center',
'text-margin-y': '4px',
};
return [
// Default node
{
selector: 'node',
style: {
...label,
shape: 'ellipse',
width: '36px',
height: '36px',
'background-color': palette.contentFill,
'border-width': '0px',
'cursor': 'pointer',
},
},
// Ego node (focused)
{
selector: `node[id="${egoId}"]`,
style: {
width: '48px',
height: '48px',
'border-width': '3px',
'border-color': palette.egoStroke,
'font-size': '13px',
'font-weight': 'bold',
},
},
// Ontology variants
{ selector: 'node[level="Axiom"]', style: { 'background-color': palette.axiomFill, shape: 'diamond' } },
{ selector: 'node[level="Tension"]', style: { 'background-color': palette.tensionFill, shape: 'triangle' } },
{ selector: 'node[level="Practice"]', style: { 'background-color': palette.practiceFill, shape: 'rectangle' } },
{ selector: 'node[level="Project"]', style: { 'background-color': palette.projectFill, shape: 'rectangle' } },
// ADR
{ selector: 'node[type="adr"]', style: { 'background-color': palette.adrFill, shape: 'pentagon' } },
// Hover
{
selector: 'node:selected',
style: { 'border-width': '3px', 'border-color': palette.egoStroke },
},
// Default edge
{
selector: 'edge',
style: {
width: '1.5px',
'line-color': palette.declaredEdge,
'target-arrow-color':palette.declaredEdge,
'target-arrow-shape':'triangle',
'curve-style': 'bezier',
opacity: 0.7,
},
},
// Auto-backlink edges (lighter)
{
selector: 'edge[source="auto_backlink"]',
style: { 'line-color': palette.autoEdge, 'target-arrow-color': palette.autoEdge, opacity: 0.5 },
},
// Implicit / SharedTag edges (dashed, very light)
{
selector: 'edge[kind="shared_tag"]',
style: {
'line-style': 'dashed',
'line-color': palette.implicitEdge,
'target-arrow-color':palette.implicitEdge,
opacity: 0.35,
},
},
];
}
// ── Element conversion ───────────────────────────────────────────────────────
function toCytoscapeElements(graphData) {
const nodes = (graphData.nodes || []).map(n => ({
data: {
id: n.id,
title: n.title || n.name || n.id,
type: n.type,
level: n.level || null,
url: n.url || null,
instance: n.instance || null,
},
}));
const edges = (graphData.edges || []).map((e, i) => ({
data: {
id: `e-${i}`,
source: e.from,
target: e.to,
kind: e.kind,
source_type: e.source, // "declared" | "auto_backlink" | "implicit"
},
}));
return [...nodes, ...edges];
}
// ── Layout config ────────────────────────────────────────────────────────────
function buildLayout(nodeCount) {
// cose-bilkent for larger graphs; grid fallback for tiny graphs
if (nodeCount <= 4) {
return { name: 'circle', animate: false, padding: 40 };
}
return {
name: 'cose-bilkent',
animate: false,
nodeDimensionsIncludeLabels: true,
idealEdgeLength: 100,
nodeRepulsion: 8000,
padding: 30,
};
}
// ── Instances registry ───────────────────────────────────────────────────────
// Keeps track of active cy instances so they can be destroyed on re-render.
const _instances = new Map();
// ── Public API ───────────────────────────────────────────────────────────────
window.ContentGraph = {
/**
* Render the knowledge graph into `containerId`.
*
* @param {string} containerId - DOM id of the container element
* @param {string} focusNodeId - node id to highlight/center; may be empty
* @param {string} theme - "dark" | "light"
*/
render(containerId, focusNodeId, theme) {
const container = document.getElementById(containerId);
if (!container) {
console.warn('[ContentGraph] container not found:', containerId);
return;
}
// Destroy previous instance if re-rendering
if (_instances.has(containerId)) {
_instances.get(containerId).destroy();
_instances.delete(containerId);
}
// Read graph data embedded by SSR
const dataEl = document.getElementById('content-graph-data');
if (!dataEl) {
console.warn('[ContentGraph] #content-graph-data not found');
return;
}
let graphData;
try {
graphData = JSON.parse(dataEl.textContent);
} catch (e) {
console.error('[ContentGraph] failed to parse graph data:', e);
return;
}
const isDark = theme === 'dark';
const palette = buildPalette(isDark);
const egoId = focusNodeId || '';
const cy = window.cytoscape({
container,
elements: toCytoscapeElements(graphData),
style: buildStylesheet(palette, egoId),
layout: buildLayout((graphData.nodes || []).length),
minZoom: 0.3,
maxZoom: 4,
});
_instances.set(containerId, cy);
// Navigate to content URL on node tap
cy.on('tap', 'node', evt => {
const url = evt.target.data('url');
if (url) window.location.href = url;
});
// Pointer cursor on hover
cy.on('mouseover', 'node[?url]', () => { container.style.cursor = 'pointer'; });
cy.on('mouseout', 'node', () => { container.style.cursor = 'default'; });
// Focus and center on ego node
if (egoId) {
const egoNode = cy.$(`#${CSS.escape(egoId)}`);
if (egoNode.length) {
cy.animate({ fit: { eles: egoNode.neighborhood().add(egoNode), padding: 60 } }, { duration: 400 });
}
}
},
/**
* Destroy the Cytoscape instance for a given container (cleanup on unmount).
*/
destroy(containerId) {
if (_instances.has(containerId)) {
_instances.get(containerId).destroy();
_instances.delete(containerId);
}
},
};
})();

File diff suppressed because one or more lines are too long

32
site/site/_public/js/cytoscape.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,48 @@
// Additional highlight.js utilities (auto-init is handled by bundle)
window.highlightCode = function() {
// Find all code blocks that haven't been highlighted yet
document.querySelectorAll('pre code:not(.hljs)').forEach(function(block) {
if (typeof hljs !== 'undefined' && hljs.highlightElement) {
hljs.highlightElement(block);
}
});
};
window.highlightContainer = function(containerSelector) {
// Highlight code blocks within a specific container
const container = document.querySelector(containerSelector);
if (container && typeof hljs !== 'undefined') {
container.querySelectorAll('pre code:not(.hljs)').forEach(function(block) {
hljs.highlightElement(block);
});
}
};
// Re-highlight when content changes (for dynamic content)
document.addEventListener('DOMContentLoaded', function() {
const observer = new MutationObserver(function(mutations) {
let shouldHighlight = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Check if any added nodes contain code blocks
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 && (node.tagName === 'PRE' || node.querySelector('pre'))) {
shouldHighlight = true;
}
});
}
});
if (shouldHighlight) {
setTimeout(function() {
window.highlightCode();
}, 100);
}
});
// Start observing document changes
observer.observe(document.body, {
childList: true,
subtree: true
});
});

View file

@ -0,0 +1 @@
window.highlightCode=function(){document.querySelectorAll('pre code:not(.hljs)').forEach(function(block){if(typeof hljs!=='undefined'&&hljs.highlightElement){hljs.highlightElement(block);}});};window.highlightContainer=function(containerSelector){const container=document.querySelector(containerSelector);if(container&&typeof hljs!=='undefined'){container.querySelectorAll('pre code:not(.hljs)').forEach(function(block){hljs.highlightElement(block);});}};document.addEventListener('DOMContentLoaded',function(){const observer=new MutationObserver(function(mutations){let shouldHighlight=false;mutations.forEach(function(mutation){if(mutation.type==='childList'&&mutation.addedNodes.length>0){mutation.addedNodes.forEach(function(node){if(node.nodeType===1&&(node.tagName==='PRE'||node.querySelector('pre'))){shouldHighlight=true;}});}});if(shouldHighlight){setTimeout(function(){window.highlightCode();},100);}});observer.observe(document.body,{childList: true,subtree: true});});

View file

@ -0,0 +1,130 @@
/**
* htmx-reinit.js re-initialise non-HTMX scripts after every HTMX swap.
*
* In the htmx-ssr rendering profile, scripts like highlight.js, cytoscape,
* theme-init, and tag-filters run once at page load. When HTMX swaps a
* fragment in, those scripts don't see the new DOM and the content goes
* "dead" (unhighlighted code blocks, unwired filter pills, etc.).
*
* This module fixes that by exposing a registry
* `window.RusteloReinit.register(fn)` and a single listener on
* `htmx:afterSettle` that walks the registry and calls each handler with
* the swapped element as scope.
*
* Handlers MUST be idempotent. We help by tracking processed nodes in a
* WeakSet keyed by handler id, so double-decoration is impossible even if
* a swap re-emits a region that contains an already-processed node.
*
* Usage from another script:
*
* (function () {
* window.RusteloReinit.register('highlight', function (scope) {
* scope.querySelectorAll('pre code:not(.hljs)').forEach(hljs.highlightElement);
* });
* })();
*
* The registered name is used to namespace the processed-nodes WeakSet so
* each handler manages its own idempotency.
*/
(function () {
'use strict';
if (window.RusteloReinit) {
return; // already loaded
}
/** @type {Array<{ name: string, fn: (scope: Element) => void, seen: WeakSet }>} */
const handlers = [];
function findHandler(name) {
for (let i = 0; i < handlers.length; i++) {
if (handlers[i].name === name) {
return handlers[i];
}
}
return null;
}
function runHandler(handler, scope) {
if (handler.seen.has(scope)) {
return;
}
handler.seen.add(scope);
try {
handler.fn(scope);
} catch (err) {
console.error('[RusteloReinit:' + handler.name + '] handler failed', err);
}
}
const api = {
/**
* Register a re-init handler. Calling register twice with the same
* name replaces the previous handler.
*/
register: function (name, fn) {
if (typeof name !== 'string' || typeof fn !== 'function') {
throw new TypeError('RusteloReinit.register(name, fn) — name must be string, fn must be function');
}
const existing = findHandler(name);
if (existing) {
existing.fn = fn;
// Reset the seen set so the replacement gets a fresh chance at the DOM.
existing.seen = new WeakSet();
} else {
handlers.push({ name: name, fn: fn, seen: new WeakSet() });
}
// Run immediately on document.body so the initial page gets processed.
if (document.body) {
runHandler(findHandler(name), document.body);
} else {
document.addEventListener('DOMContentLoaded', function () {
runHandler(findHandler(name), document.body);
}, { once: true });
}
},
/**
* Force-run every handler against the given scope. Used by tests and by
* the htmx:afterSettle listener.
*/
runAll: function (scope) {
for (let i = 0; i < handlers.length; i++) {
runHandler(handlers[i], scope);
}
},
/** Return registered handler names (debug). */
names: function () {
return handlers.map(function (h) { return h.name; });
}
};
window.RusteloReinit = api;
// Hook into htmx lifecycle events. afterSettle fires after the DOM is in
// its final state for the swap; we want this rather than afterSwap to
// avoid running against elements that get replaced moments later.
document.body && document.body.addEventListener('htmx:afterSettle', function (ev) {
const target = ev.detail && ev.detail.target;
if (target instanceof Element) {
api.runAll(target);
} else {
api.runAll(document.body);
}
});
// If body wasn't ready yet, wire on DOMContentLoaded.
if (!document.body) {
document.addEventListener('DOMContentLoaded', function () {
document.body.addEventListener('htmx:afterSettle', function (ev) {
const target = ev.detail && ev.detail.target;
if (target instanceof Element) {
api.runAll(target);
} else {
api.runAll(document.body);
}
});
}, { once: true });
}
})();

View file

@ -0,0 +1,114 @@
/**
* reinit-handlers.js registers the standard set of HTMX swap re-initialisers.
*
* Loaded AFTER htmx-reinit.js and after the underlying libraries (hljs,
* cytoscape) so the handler bodies can call them directly. Each handler
* scopes its DOM queries to the swap target passing an Element to
* querySelectorAll only searches that subtree, so handlers are O(swap)
* rather than O(document) on every swap.
*/
(function () {
'use strict';
if (!window.RusteloReinit) {
console.error('[reinit-handlers] htmx-reinit.js must load before this script');
return;
}
// --- highlight.js -------------------------------------------------------
// Highlight every <pre><code> in the swapped region that isn't already
// decorated. hljs marks processed blocks by adding the .hljs class, which
// makes the selector self-cleaning on re-runs.
window.RusteloReinit.register('highlight', function (scope) {
if (typeof window.hljs === 'undefined' || !window.hljs.highlightElement) {
return;
}
scope.querySelectorAll('pre code:not(.hljs)').forEach(function (block) {
window.hljs.highlightElement(block);
});
});
// --- theme-init ---------------------------------------------------------
// Normalise the theme state on <html> from the cookie after every swap.
// This is the single authority that reconciles the theme-toggle's inline
// script (which adds `.dark` but never clears the stale `.light`, and the
// reverse) into a consistent state: mutually-exclusive class AND the
// `data-theme` attribute. The attribute is what DaisyUI keys its base
// tokens on (`:root,[data-theme]{color:hsl(var(--bc))}`); without it the
// light `--bc` never resolves and low-opacity text renders invisible.
// Mirrors theme-init.js so first paint and post-swap agree. Idempotent.
window.RusteloReinit.register('theme', function (_scope) {
const matches = document.cookie.match(/theme=([^;]+)/);
const theme = matches ? matches[1] : 'dark';
const resolved = theme === 'dark' ||
(theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
? 'dark'
: 'light';
const html = document.documentElement;
html.classList.toggle('dark', resolved === 'dark');
html.classList.toggle('light', resolved === 'light');
html.dataset.theme = resolved;
});
// --- tag-filters --------------------------------------------------------
// Re-arm the filter pill click handlers in the swapped region. The
// legacy script set window.tagFiltersInitialized = true and bailed on
// subsequent runs; bypass that flag by re-binding only pills that don't
// yet carry our data-reinit attribute.
window.RusteloReinit.register('tag-filters', function (scope) {
if (typeof window.filterPostsByTag !== 'function') {
return;
}
scope.querySelectorAll('[data-tag-pill]:not([data-tag-reinit])').forEach(function (pill) {
pill.setAttribute('data-tag-reinit', '1');
pill.addEventListener('click', function (ev) {
ev.preventDefault();
const tag = pill.getAttribute('data-tag');
if (tag) {
window.filterPostsByTag(tag);
}
});
});
});
// --- scroll-to-top -------------------------------------------------------
// Wires the footer "Scroll to top" button emitted by UnifiedFooter (SSR).
// The Leptos on:click handler is WASM-only and compiles to nothing in SSR;
// this provides the equivalent for the HTMX profile via event delegation.
// Runs once: the button lives in the footer which is never swapped.
(function () {
if (window.__rusteloScrollToTopWired) {
return;
}
window.__rusteloScrollToTopWired = true;
document.addEventListener('click', function (ev) {
var btn = ev.target && ev.target.closest('button[aria-label="Scroll to top"]');
if (btn) {
ev.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
})();
// --- content-graph ------------------------------------------------------
// ContentGraph.render is idempotent against the same container id (it
// tears down the previous instance internally). Only fire when the
// swap actually contains a graph container.
window.RusteloReinit.register('content-graph', function (scope) {
if (!window.ContentGraph || typeof window.ContentGraph.render !== 'function') {
return;
}
scope.querySelectorAll('[data-content-graph]').forEach(function (el) {
const containerId = el.getAttribute('id');
const focusId = el.getAttribute('data-focus-id') || null;
const theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
if (containerId) {
try {
window.ContentGraph.render(containerId, focusId, theme);
} catch (err) {
console.error('[reinit:content-graph] render failed', err);
}
}
});
});
})();

Some files were not shown because too many files have changed in this diff Show more