This commit is contained in:
2026-09-22 05:19:09 -03:00
parent 54c577a700
commit 88adcd68bc
25 changed files with 862 additions and 248 deletions

View File

@@ -58,8 +58,8 @@ start: ## run a built room [<room>] [-d] [--build]
stop: ## stop a running room [<room>]
bash ctrl/stop.sh $(or $(ARGS),$(ROOM))
dist: ## compile the plexus UIs to single files [<room>]
bash ctrl/dist.sh $(or $(ARGS),$(ROOM))
dist: ## compile the plexus UIs to single files [NAME|FILE]
bash ctrl/dist.sh $(ARGS)
# ── theme ──────────────────────────────────────────────────────────────────

184
build.py
View File

@@ -23,7 +23,6 @@ Generated structure for managed rooms:
"""
import argparse
import importlib.util
import json
import logging
import shutil
@@ -559,148 +558,6 @@ def _append_cabinet_env(output_dir: Path, cabinets: list[dict]):
example.write_text(existing.rstrip("\n") + "\n" + "\n".join(lines) + "\n")
def load_plexuses(room: str) -> list[dict]:
"""The plexuses a room asked for. Same shape as its sibling data/*.json."""
path = SPR_ROOT / "cfg" / room / "data" / "plexuses.json"
if not path.exists():
return []
try:
raw = json.loads(path.read_text())
except ValueError as e:
log.warning(f" plexuses.json is not valid JSON, ignoring: {e}")
return []
entries = raw.get("items", raw) if isinstance(raw, dict) else raw
out = []
for entry in entries if isinstance(entries, list) else []:
if isinstance(entry, str):
entry = {"name": entry}
if isinstance(entry, dict) and entry.get("name"):
out.append(entry)
return out
def _theme_css(theme: str) -> str:
"""The token contract plus one theme, flattened for inlining.
Only the named theme ships alongside the others it can switch to, because
the export has to work with no server: there is no /theme.css to fetch.
"""
theme_dir = SPR_ROOT / "soleprint" / "common" / "theme"
parts = []
tokens = theme_dir / "tokens.css"
if tokens.exists():
parts.append(tokens.read_text())
# Every theme, so the switcher in the page has something to switch to.
for sheet in sorted((theme_dir / "themes").glob("*.css")):
parts.append(sheet.read_text())
return "\n".join(parts)
def _inline_svg(name: str, theme: str) -> str:
"""A rendered graph, stripped of its XML prolog so it can sit in HTML.
Inlined rather than <img>-linked so the page's CSS can recolour it when the
theme switches — graphviz writes class="node accent" into the SVG, and CSS
outranks the presentation attributes it bakes in.
"""
graphs = SPR_ROOT / "docs" / "graphs"
for candidate in (graphs / f"{name}.{theme}.svg", graphs / f"{name}.svg"):
if candidate.exists():
svg = candidate.read_text()
start = svg.find("<svg")
return svg[start:] if start >= 0 else svg
log.warning(f" no rendered graph '{name}' — run docs/graphs/render.sh")
return "<p>diagram not rendered</p>"
def build_plexuses(output_dir: Path, room: str):
"""Export each plexus the room declared to a single self-contained file.
A plexus is exported, not served. The output is one index.html carrying its
theme, its data and its diagram, so it survives a locked-down machine, a zip
attachment and a double-click — which is the whole point of the format.
"""
requested = load_plexuses(room)
if not requested:
return
source_root = SPR_ROOT / "soleprint" / "artery" / "plexuses"
built = []
for entry in requested:
name = entry["name"]
source = source_root / name
manifest_path = source / "plexus.json"
if not manifest_path.exists():
available = sorted(
p.name for p in source_root.iterdir() if p.is_dir()
) if source_root.exists() else []
log.warning(
f" no such plexus: {name} (available: {', '.join(available) or 'none'})"
)
continue
try:
manifest = json.loads(manifest_path.read_text())
except ValueError as e:
log.warning(f" plexus {name} has invalid plexus.json: {e}")
continue
# The room may override anything the plexus declares — theme first.
manifest.update({k: v for k, v in entry.items() if k != "name"})
template_path = source / "app" / "index.html"
if not template_path.exists():
log.warning(f" plexus {name} has no app/index.html")
continue
theme = manifest.get("theme", "soleprint")
data = {k: v for k, v in manifest.items() if not k.startswith("_")}
# A plexus may ship a showcase.py exposing collect(): anything it returns
# is merged into the page's data. The bundle uses it to run the real
# tools over the real fixtures at build time, so what the page shows
# cannot drift from what the tools do.
collector = source / "showcase.py"
if collector.exists():
try:
spec = importlib.util.spec_from_file_location(
f"plexus_{name}_showcase", collector
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
data["showcase"] = module.collect()
except Exception as e:
log.warning(f" {name}: showcase.py failed ({type(e).__name__}: {e})")
page = template_path.read_text()
for token, value in (
("%%TITLE%%", manifest.get("title", name)),
("%%DESCRIPTION%%", manifest.get("description", "")),
("%%DEFAULT_THEME%%", theme),
("%%BUILT%%", f"{room} · built by soleprint build.py"),
("%%THEME_CSS%%", _theme_css(theme)),
("%%GRAPH%%", _inline_svg(manifest.get("graph", "system_overview"), theme)),
("%%BUNDLE%%", json.dumps(data, indent=2)),
):
page = page.replace(token, value)
target = output_dir / "plexuses" / name
ensure_dir(target)
(target / "index.html").write_text(page)
# Anything else in app/ rides along, for a plexus that outgrows one file.
for extra in (source / "app").iterdir():
if extra.name != "index.html":
copy_path(extra, target / extra.name, quiet=True)
built.append(f"{name} ({theme})")
if built:
log.info(f" plexuses: {', '.join(built)}")
def build_soleprint(output_dir: Path, room: str):
"""Build soleprint folder with core + room config merged."""
soleprint = SPR_ROOT / "soleprint"
@@ -738,11 +595,6 @@ def build_soleprint(output_dir: Path, room: str):
log.info("Composing cabinets...")
compose_cabinets(output_dir, room)
# Plexuses are exported rather than served, so this is a compile step like
# the cabinet merge above — not something run.py does at request time.
log.info("Exporting plexuses...")
build_plexuses(output_dir, room)
# Generate models
log.info("Generating models...")
if not generate_models(output_dir, room):
@@ -799,33 +651,6 @@ def build_models_only():
sys.exit(1)
def build_plexuses_only(room: str):
"""Compile just the plexus UIs, without rebuilding the room around them.
The equivalent of `vite build` for this repo: the iteration loop when you
are working on the UI itself is edit, compile, reopen the file — and a full
room build to see a CSS change is a slow way to do that.
"""
output_dir = SPR_ROOT / "gen" / room
if not output_dir.exists():
log.error(f"Room '{room}' is not built — run: python build.py --cfg {room}")
sys.exit(1)
log.info(f"Compiling plexus UIs for {room}...")
build_plexuses(output_dir, room)
built = sorted((output_dir / "plexuses").glob("*/index.html"))
if not built:
log.warning(
f" nothing compiled — does cfg/{room}/data/plexuses.json list one?"
)
return
for page in built:
log.info(f" {page.relative_to(SPR_ROOT)} ({page.stat().st_size // 1024} KB)")
log.info("\n✓ Open directly — no server needed:")
log.info(f" xdg-open {built[0]}")
def main():
parser = argparse.ArgumentParser(description="Soleprint Build Tool")
@@ -833,17 +658,10 @@ def main():
parser.add_argument("--cfg", "-c", type=str, help="Room config name")
parser.add_argument("--all", action="store_true", help="Build all rooms")
parser.add_argument("--models", action="store_true", help="Only regenerate models")
parser.add_argument(
"--plexuses",
action="store_true",
help="Only compile the plexus UIs into an already-built room",
)
args = parser.parse_args()
if args.plexuses:
build_plexuses_only(args.cfg or "standalone")
elif args.models:
if args.models:
build_models_only()
elif args.all:
build(SPR_ROOT / "gen" / "standalone", None)

View File

@@ -1,3 +0,0 @@
{
"items": []
}

View File

@@ -1,7 +0,0 @@
{
"items": [
{
"name": "bundle"
}
]
}

View File

@@ -2,30 +2,36 @@
# Compile the plexus UIs to distributable files — this repo's `vite build`.
#
# Usage:
# ./ctrl/dist.sh # standalone
# ./ctrl/dist.sh sample # a named room
# ./ctrl/dist.sh # every plexus -> gen/plexuses/<name>/index.html
# ./ctrl/dist.sh bundle # one, by name
# ./ctrl/dist.sh path/to/ui.toml # every [[plexus]] a run file lists
# ./ctrl/dist.sh bundle --dest /tmp/out --delivery vendored --notice none
#
# A plexus is a UI that gets EXPORTED, not served. The output is a single
# index.html carrying its theme, its data and its diagrams inline, so it opens
# from a double-click on a machine with no server, no node and no network — the
# state a regulated Windows box is usually in.
#
# `make build` runs this as one of its steps. This exists for the loop where the
# UI is what you are working on: rebuilding a whole room to see a CSS change is
# a slow way to iterate.
# It needs nothing built first: a plexus is its own directory under
# soleprint/artery/plexuses/ (or wherever a run file points), and the theme it
# carries is cut to what the page reads. See soleprint/common/slice/plexus.py.
set -e
CALLER_DIR="$PWD"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$ROOT_DIR"
PYTHON="${PYTHON:-python3}"
ROOM="${1:-standalone}"
if [[ ! -d "cfg/$ROOM" ]]; then
echo "No such room: cfg/$ROOM" >&2
echo "Available: $(find cfg -mindepth 1 -maxdepth 1 -type d -not -name '.*' -printf '%f ')" >&2
exit 1
args=()
for a in "$@"; do
# A bare word that names an existing path is a path the caller typed.
if [[ "$a" != -* && -e "$CALLER_DIR/$a" ]]; then
args+=("$(cd "$CALLER_DIR" && realpath -m -- "$a")")
else
args+=("$a")
fi
done
exec "$PYTHON" build.py --plexuses --cfg "$ROOM"
cd "$ROOT_DIR/soleprint"
exec "$PYTHON" -m common.slice plexus "${args[@]}"

View File

@@ -6,17 +6,17 @@ is **exported, not served**. It is compiled to a distributable file the way vite
builds for production, and that compile is the whole point of the format.
```bash
make dist # compile the plexus UIs for standalone
make dist sample # for a named room
make dist # every plexus
make dist bundle # one, by name
make dist path/to/ui.toml # the [[plexus]] tables of a run file
```
Output is one `index.html` per plexus under `gen/<room>/plexuses/<name>/`,
Output is one `index.html` per plexus under `gen/plexuses/<name>/` (or `--dest`),
carrying its theme, its data and its diagrams inline. No server, no node, no
network. Zip it, mail it, double-click it.
`make build` runs the same step as part of a room build. `make dist` exists for
the loop where the UI is what you are working on — rebuilding a whole room to
see a CSS change is a slow way to iterate.
A plexus is built on its own: nothing else has to be built first, and no other
configuration has to name it.
## The constraint that shapes it
@@ -34,20 +34,22 @@ So the data is a JS object, the theme is inlined at compile time, and the fonts
are stacks. The test that matters is opening the output with the network off and
seeing zero failed requests — everything else is cosmetic.
## Declaring one
## Choosing what gets built
A room opts in through `cfg/<room>/data/plexuses.json`, the same shape and the
same place as its sibling `data/*.json` files:
`make dist` builds every directory under `soleprint/artery/plexuses/` that holds a
`plexus.json`. To build with other settings — another theme, a plexus kept
outside spr, a different destination — list it in a run file:
```json
{ "items": [ { "name": "bundle" } ] }
```toml
[[plexus]]
name = "bundle"
theme = "mcrn"
dest = "out/bundle.html"
```
A room may override anything the plexus declares — most usefully the theme:
```json
{ "items": [ { "name": "bundle", "theme": "mcrn" } ] }
```
`delivery = "vendored"` renames the output through the mapping profile, refuses
to write if any spr word survives, and needs a `notice` — the same rules as a
vendored slice. See [Export / Compile](#export).
## Writing one
@@ -57,17 +59,22 @@ soleprint/artery/plexuses/<name>/
app/index.html the template
```
`build.py` fills these placeholders and writes one file:
`make dist` fills these placeholders and writes one file:
| Placeholder | Becomes |
| --- | --- |
| `%%THEME_CSS%%` | tokens plus every theme, so the switcher has something to switch to |
| `%%THEME_CSS%%` | only what the page can use: the custom properties it reads (and what they reach) per theme in `inline_themes`, and the rules that can match something on it |
| `%%BUNDLE%%` | `plexus.json` as a JS object |
| `%%GRAPH%%` | a rendered SVG from `docs/graphs/`, inlined |
| `%%TITLE%%` `%%DESCRIPTION%%` `%%DEFAULT_THEME%%` `%%BUILT%%` | from the manifest |
Anything else in `app/` is copied alongside, for a plexus that outgrows one file.
A plexus whose `app/` holds a `package.json` is built as a **Vue app** instead:
its `@ui` imports are sliced from `common/ui` (the same closure a consumer
would get), `vite build` runs in a temporary directory, and the emitted script
and stylesheet are inlined into one file. `live/` is the example.
## The bundle plexus
The one that ships. It answers "what does a rig installation have at its

View File

@@ -19,7 +19,7 @@ make start # run it
| Command | Runs | Does |
| --- | --- | --- |
| `make build [<room>\|all\|models]` | `ctrl/build.sh` | compile a room into `gen/` |
| `make dist [<room>]` | `ctrl/dist.sh` | compile just the plexus UIs to single files |
| `make dist [NAME\|FILE]` | `ctrl/dist.sh` | compile the plexus UIs to single files — nothing built first |
| `make docs [serve\|graphs]` | `ctrl/docs.sh` | serve the docs, or re-render the diagrams |
| `make start [<room>] [-d]` | `ctrl/start.sh` | run a built room's compose stack |
| `make stop [<room>]` | `ctrl/stop.sh` | stop it |
@@ -52,12 +52,9 @@ make component ARGS="publish soleprint-ui /tmp/out --dist"
4. **Compose cabinets.** The dependency containers the room declared in
`data/cabinets.json` are merged into its `docker-compose.yml`. See
[Cabinets](#station-cabinets).
5. **Export plexuses.** Each plexus the room declared in `data/plexuses.json` is
compiled to a single self-contained `index.html` — theme, data and diagrams
inlined, so it opens with no server. See [Plexuses](#artery-plexuses).
6. **Generate models.** modelgen reads the room's `config.json` and writes
5. **Generate models.** modelgen reads the room's `config.json` and writes
`models/pydantic/__init__.py`.
7. **Render k8s** (optional). When the room's config enables it,
6. **Render k8s** (optional). When the room's config enables it,
`soleprint/ctrl/k8s/` writes manifests and lifecycle scripts.
## What comes out
@@ -71,7 +68,6 @@ gen/standalone/
cfg/config.json
data/*.json
models/pydantic/
plexuses/<name>/index.html # one file each, opens with no server
```
A **managed** room — one that wraps an existing application — is three folders

View File

@@ -5,12 +5,14 @@ A plexus is a full app — and, unlike everything else here, one that is
vite builds for production.
```bash
make dist # standalone
make dist sample # a named room
make dist # every plexus here
make dist bundle # one, by name
make dist path/to/ui.toml # the [[plexus]] tables of a run file
```
Output: `gen/<room>/plexuses/<name>/index.html`, one self-contained file with its
theme, its data and its diagrams inlined.
Output: `gen/plexuses/<name>/index.html` (or `--dest`), one self-contained file
with its theme, its data and its diagrams inlined. Nothing has to be built first:
a plexus is its own directory, and `soleprint/common/slice/plexus.py` builds it.
## The constraint
@@ -39,15 +41,22 @@ failed requests. Everything else is cosmetic.
| Placeholder | Becomes |
| --- | --- |
| `%%THEME_CSS%%` | tokens plus every theme, so the switcher has somewhere to go |
| `%%THEME_CSS%%` | the tokens and rules the page can use, for each theme in `inline_themes` (default: `theme` alone) |
| `%%BUNDLE%%` | `plexus.json` as a JS object literal |
| `%%GRAPH%%` | a rendered SVG from `docs/graphs/`, prolog stripped, inlined |
| `%%TITLE%%` `%%DESCRIPTION%%` `%%DEFAULT_THEME%%` `%%BUILT%%` | from the manifest |
Anything else in `app/` is copied alongside, for a plexus that outgrows one file.
A room declares which ones it wants in `cfg/<room>/data/plexuses.json`, and may
override anything the manifest sets — most usefully `theme`.
A run file's `[[plexus]]` table can override `theme`, `inline_themes`, `title`
and `description`, point `source` at a plexus kept elsewhere, and set `dest`.
`delivery = "vendored"` renames the output through the mapping profile, fails on
any spr word, and needs a `notice` — the same rules as a vendored slice.
A plexus whose `app/` holds a `package.json` is a **Vue** plexus: its `@ui`
imports are sliced from `common/ui`, `vite build` runs in a temporary directory,
and the script and stylesheet are inlined into the one file. `live/` is the
example.
## Why the diagram is inlined
@@ -61,5 +70,5 @@ diagram become something you could put in a document.
## Adding one
Drop a directory in beside `bundle/`. Nothing dispatches on the name — the room's
`plexuses.json` is the only place it has to be mentioned.
Drop a directory in beside `bundle/`. Nothing dispatches on the name and nothing
lists it: `make dist` builds every directory that holds a `plexus.json`.

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<!--
Bundle plexus — the template. build.py inlines everything marked %%…%% and
writes one self-contained file to gen/<room>/plexuses/bundle/index.html.
Bundle plexus — the template. `make dist bundle` inlines everything marked
%%…%% and writes one self-contained file to gen/plexuses/bundle/index.html.
What this page is FOR: showing the three themes doing real work. Every panel
renders genuine output — showcase.py runs modelgen, datagen and shuntgen over
@@ -217,7 +217,7 @@ if (S.shunt) {
if (S.cabinet) {
out.push('<h2>Dependencies it declares <span class="n">— cabinets</span></h2>');
out.push('<p class="lede">A room asks for <code>' + esc(S.cabinet.name) +
out.push('<p class="lede">An environment asks for <code>' + esc(S.cabinet.name) +
'</code> once. This is merged into its compose file on a laptop; the same name ' +
'installs as a rig addon in a cluster.</p>');
out.push('<div class="panel"><pre>' + esc(S.cabinet.fragment) + "</pre></div>");

View File

@@ -3,6 +3,7 @@
"title": "Soleprint Bundle",
"description": "What a rig installation has at its disposal. Exports to a single file that opens with no server.",
"theme": "lucid",
"inline_themes": ["lucid", "soleprint", "mcrn"],
"graph": "system_overview",
"_comment": "A plexus is exported, not served. build.py inlines the theme, the data and the diagram into one index.html so it survives a locked-down Windows box, a zip attachment and a double-click. Nothing here is fetched at runtime.",

View File

@@ -151,7 +151,7 @@ def _shunt(tmp: Path) -> dict | None:
def _cabinet() -> dict | None:
"""A cabinet's compose fragment, as it gets merged into a room."""
"""A cabinet's compose fragment, as it gets merged into a compose file."""
path = SOLEPRINT / "station" / "cabinets" / "postgres" / "service.yml"
if not path.exists():
return None

View File

@@ -0,0 +1,3 @@
node_modules/
dist/
ui/

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Live panels</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
{
"name": "live-panels",
"private": true,
"type": "module",
"dependencies": {
"vue": "^3.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5",
"vite": "^6"
}
}

View File

@@ -0,0 +1,4 @@
# pnpm 10+ refuses dependency build scripts unless they are allowed here.
# esbuild (vite's bundler) needs its postinstall to link a platform binary.
allowBuilds:
esbuild: true

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
/*
* A live page with no server: a local generator stands in for a stream, so
* the file behaves the same from a double-click as it would behind an API.
* Everything drawn comes from the slice: Panel, SplitPane, LogRenderer.
*/
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { LogRenderer, Panel, SplitPane, type LogEntry } from '@ui'
const STAGES = ['ingest', 'validate', 'transform', 'load']
const LEVELS = ['INFO', 'INFO', 'INFO', 'DEBUG', 'WARN', 'ERROR']
const MAX_LINES = 2000
const entries = ref<LogEntry[]>([])
const counts = ref<Record<string, number>>({})
const running = ref(true)
let timer: number | undefined
function tick(): void {
const level = LEVELS[Math.floor(Math.random() * LEVELS.length)]
const stage = STAGES[Math.floor(Math.random() * STAGES.length)]
const n = entries.value.length + 1
entries.value.push({ level, stage, msg: `record ${n} ${level === 'ERROR' ? 'rejected' : 'ok'}`,
ts: new Date().toISOString().slice(11, 23) })
if (entries.value.length > MAX_LINES) entries.value.splice(0, entries.value.length - MAX_LINES)
counts.value[level] = (counts.value[level] ?? 0) + 1
}
function toggle(): void {
running.value = !running.value
if (running.value) start()
else stop()
}
function start(): void { timer = window.setInterval(tick, 50) }
function stop(): void { window.clearInterval(timer) }
const status = computed(() => (running.value ? 'live' : 'idle'))
onMounted(start)
onUnmounted(stop)
</script>
<template>
<div class="page">
<SplitPane direction="horizontal" :initial-size="2">
<template #first>
<Panel title="Events" :status="status">
<template #actions>
<button @click="toggle">{{ running ? 'pause' : 'resume' }}</button>
</template>
<LogRenderer :entries="entries" />
</Panel>
</template>
<template #second>
<Panel title="Counts" :maximizable="false">
<dl class="counts">
<template v-for="level in ['INFO', 'DEBUG', 'WARN', 'ERROR']" :key="level">
<dt>{{ level }}</dt>
<dd>{{ counts[level] ?? 0 }}</dd>
</template>
</dl>
</Panel>
</template>
</SplitPane>
</div>
</template>
<style>
html, body, #app { height: 100%; margin: 0; }
.page { height: 100%; padding: var(--space-3); box-sizing: border-box; }
.counts { display: grid; grid-template-columns: auto 1fr; gap: var(--space-2) var(--space-4);
font-family: var(--font-mono); color: var(--text-primary); margin: 0; }
.counts dt { color: var(--text-secondary); }
.counts dd { margin: 0; text-align: right; }
</style>

View File

@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, unknown>
export default component
}

View File

@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View File

@@ -0,0 +1,6 @@
{
"name": "live",
"title": "Live panels",
"description": "A panel layout fed by a local generator, built from a slice of the Vue library into one file that opens with no server.",
"_comment": "A Vue plexus: app/ is an ordinary Vite + Vue app whose `@ui` imports are sliced from common/ui at build time. `make dist live` builds it in a temporary directory and inlines the script and stylesheet, so the output is one file. Nothing here is fetched at runtime; the generator stands in for a stream."
}

View File

@@ -9,6 +9,7 @@ The command line — every command slices have, and nothing else.
scan what a consumer uses, and how its framework copy compares with spr
slice write every slice a run file lists (only what is stale)
check the same, writing nothing: exit 1 if anything is stale
plexus build one self-contained page from a plexus (make dist)
selftest this package's own suite
Library modules beside this package hold no command-line code; selftest asserts
@@ -23,6 +24,7 @@ COMMANDS = {
"scan": ("scan", "what a consumer uses, and its copy against spr"),
"slice": ("slice", "write every slice a run file lists"),
"check": ("slice", "the same, writing nothing — exit 1 if stale"),
"plexus": ("plexus", "build a plexus: one self-contained page"),
"selftest": ("selftest", "this package's own suite"),
}

View File

@@ -0,0 +1,71 @@
"""
python3 -m common.slice plexus every plexus -> gen/plexuses/<name>/
python3 -m common.slice plexus NAME [--dest PATH] one, by name
python3 -m common.slice plexus FILE [--only NAME] every [[plexus]] in a run file
… --delivery vendored --notice none|FILE renamed, scrubbed, notice stated
No instance to build first, nothing keyed on configuration elsewhere: a plexus
is its own directory, and this writes one self-contained file from it.
"""
import argparse
import re
from pathlib import Path
from .. import plexus as px
from .common import PROG, Abort, kb, load_runfile
EXTERNAL = re.compile(r"""(?:src|href)=["'](?!#|data:|javascript:)([^"']+)["']""")
def main(argv, command=None) -> int:
p = argparse.ArgumentParser(prog=f"{PROG} plexus")
p.add_argument("target", nargs="?", help="A plexus name, or a run file. Omit for every plexus.")
p.add_argument("--only", action="append", default=[], metavar="NAME",
help="With a run file: just this plexus. Repeatable.")
p.add_argument("--dest", help="Where the .html goes (a file, or a directory for index.html).")
p.add_argument("--delivery", choices=("dependency", "vendored"), default="dependency")
p.add_argument("--notice", help='With --delivery vendored: "none", or a file to head the page.')
p.add_argument("--minify", action="store_true", help="Vue plexus: minify the inlined script.")
args = p.parse_args(argv)
try:
if args.target and args.target.endswith(".toml"):
rf = load_runfile(Path(args.target))
chosen = px.from_runfile(rf.plexuses, rf.path.resolve().parent)
if args.only:
chosen = [x for x in chosen if x.name in set(args.only)]
if not chosen:
raise Abort(f"{args.target} lists no [[plexus]] to build")
else:
names = [args.target] if args.target else px.discover()
chosen = [px.resolve(n, dest=args.dest if args.target else None,
delivery=args.delivery, notice=args.notice,
minify=args.minify) for n in names]
except px.PlexusError as e:
raise Abort(str(e)) from None
failed = 0
for x in chosen:
kind = "vue" if x.is_vue else "template"
try:
built = px.build(x)
path = px.write(built)
except px.PlexusError as e:
failed += 1
print(f" FAIL {x.name:<12} {e}")
for line, term, text in (built.hits if "built" in locals() else [])[:10]:
print(f" line {line} {term!r} {text[:90]}")
continue
external = sorted(set(EXTERNAL.findall(built.html)))
print(f" ok {x.name:<12} {kind:<8} {x.delivery:<10} {kb(len(built.html.encode())):>9} -> {path}")
for note in built.notes:
print(f" {note}")
if external:
print(f" WARNING references outside the file: {', '.join(external[:5])}")
print()
if failed:
print(f"{failed} of {len(chosen)} plexus(es) failed")
return 1
print(f"{len(chosen)} plexus(es) built")
return 0

View File

@@ -0,0 +1,147 @@
"""
Top-level CSS rules, and which of them a page can use.
A plexus used to inline the whole theme contract and every theme — 20 KB of
rules for a page that uses a fraction of them, including component styling for
screens it does not have. This cuts that down, conservatively:
custom properties only the names the page reads, plus what they reach
a rule kept unless the page provably cannot use it: every
class and id in one of its selectors has to appear
somewhere in the page — markup *or* script, since a
page that builds its DOM in JS names its classes there
element selectors kept (h2, button, *): cheap, and a page has elements
@keyframes kept when a kept rule or the page names it
@media / @supports kept when anything inside is
When unsure it keeps. A rule too many costs bytes; a rule too few is a page that
renders wrong, and nothing would say so.
"""
import re
from dataclasses import dataclass
COMMENT = re.compile(r"/\*.*?\*/", re.S)
DECL = re.compile(r"(--[\w-]+)\s*:\s*([^;]+);")
VAR = re.compile(r"var\(\s*(--[\w-]+)")
THEME_ATTR = re.compile(r"""\[data-theme=["']?[\w-]+["']?\]""")
@dataclass
class Rule:
prelude: str # selector list, or an at-rule's head
body: str # what is between the braces
@property
def at(self) -> str:
return self.prelude.split()[0] if self.prelude.startswith("@") else ""
def text(self) -> str:
return f"{self.prelude} {{{self.body}}}"
def rules(css: str) -> list[Rule]:
"""Top-level rules, comments removed. Nested blocks stay inside their body."""
css = COMMENT.sub("", css)
out, i, n = [], 0, len(css)
while i < n:
start = css.find("{", i)
if start < 0:
break
prelude = css[i:start].strip()
depth, j = 1, start + 1
while j < n and depth:
if css[j] == "{":
depth += 1
elif css[j] == "}":
depth -= 1
j += 1
out.append(Rule(prelude=prelude, body=css[start + 1:j - 1]))
i = j
return out
def only_properties(rule: Rule) -> bool:
"""A block that sets custom properties and nothing else."""
body = rule.body.strip()
return bool(body) and not DECL.sub("", body).strip(" \n\t;")
def declarations(rule: Rule) -> dict[str, str]:
return {name: value.strip() for name, value in DECL.findall(rule.body)}
def _split_selectors(prelude: str) -> list[str]:
parts, depth, cur = [], 0, ""
for ch in prelude:
if ch in "([":
depth += 1
elif ch in ")]":
depth -= 1
if ch == "," and depth == 0:
parts.append(cur)
cur = ""
else:
cur += ch
parts.append(cur)
return [p.strip() for p in parts if p.strip()]
def _names(selector: str) -> list[str]:
"""Classes and ids a selector requires, outside :not(…) and attribute brackets."""
s = THEME_ATTR.sub("", selector)
s = re.sub(r":not\([^)]*\)", "", s)
s = re.sub(r"\[[^\]]*\]", "", s)
return re.findall(r"[.#](-?[A-Za-z_][\w-]*)", s)
def matches(prelude: str, page: str) -> bool:
for selector in _split_selectors(prelude):
names = _names(selector)
if all(re.search(rf"(?<![\w-]){re.escape(n)}(?![\w-])", page) for n in names):
return True
return False
def keyframes_name(rule: Rule) -> str:
parts = rule.prelude.split()
return parts[1] if len(parts) > 1 else ""
def shake(css: str, page: str) -> tuple[list[Rule], set]:
"""Rules of `css` the page can use, and the custom properties they read.
Custom-property-only blocks are not decided here — the caller cuts those
by name — so they are returned as they are.
"""
kept, reads = [], set()
candidates = rules(css)
keyframes = [r for r in candidates if r.at == "@keyframes"]
for r in candidates:
if r.at == "@keyframes":
continue
if r.at in ("@media", "@supports"):
inner, inner_reads = shake(r.body, page)
if inner:
kept.append(Rule(prelude=r.prelude, body="\n" + "\n".join(x.text() for x in inner) + "\n"))
reads |= inner_reads
continue
if r.at:
kept.append(r) # @font-face and friends: rare, and never guessed at
continue
if only_properties(r) or matches(r.prelude, page):
kept.append(r)
reads |= set(VAR.findall(r.body))
text = page + "\n".join(r.body for r in kept)
for k in keyframes:
name = keyframes_name(k)
if name and re.search(rf"(?<![\w-]){re.escape(name)}(?![\w-])", text):
kept.append(k)
reads |= set(VAR.findall(k.body))
return kept, reads
def pretty(rule: Rule) -> str:
"""One rule, re-indented: readable, never minified."""
body = "\n".join(f" {line.strip()}" for line in rule.body.strip().splitlines() if line.strip())
return f"{rule.prelude} {{\n{body}\n}}"

View File

@@ -0,0 +1,414 @@
"""
A plexus: one page that carries everything it needs, built with nothing behind it.
make dist every plexus under artery/plexuses -> gen/plexuses/
make dist bundle one, by name
make dist path/to/ui.toml every [[plexus]] a run file lists
A plexus is exported, not served. The output is one index.html with its theme,
its data and its diagram inlined, so it survives a locked-down machine, a zip
attachment and a double-click. It is the output-only / demo-only form of the UI:
the rendered page ships, the source that made it never does.
## What changed when it stopped belonging to a build
It used to be one step of building a whole instance, keyed on which plexuses an
instance's config asked for, and it inlined the entire theme contract and every
theme — 20 KB of rules for a page that reads a fraction of them. Now:
- it is built on its own, from its directory or from a run file;
- the theme it carries is cut to what the page reads (see css.py): the custom
properties it uses and what they reach, per theme it can switch to, and the
rules that can match something on the page;
- parts arrive when the template carries `<!-- theme:here -->`, as in bake;
- `delivery = "vendored"` renames through the mapping profile and scrubs the
result, exactly as a vendored slice is — and needs a `notice`, for the same
reason.
## Two kinds
A template plexus (`app/index.html` with `%%…%%` placeholders) is filled here.
A Vue plexus (`app/package.json`) is an app built on a slice: its `@ui` imports
are sliced from common/ui, `vite build` runs in a temporary directory, and the
emitted script and stylesheet are inlined into one file — no bundler plugin,
because the inlining is a few lines of text work.
"""
import importlib.util
import json
import re
import shutil
import subprocess
import tempfile
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
from ..theme import bake
from . import css, names
from .graph import COMMON
from .manifest import REPO
PLEXUSES = COMMON.parent / "artery" / "plexuses"
THEME = COMMON / "theme"
GRAPHS = REPO / "docs" / "graphs"
GEN = REPO / "gen" / "plexuses"
KEYS = {"name", "source", "dest", "theme", "inline_themes", "delivery", "notice",
"rename", "title", "description", "minify"}
class PlexusError(ValueError):
pass
@dataclass
class Plexus:
name: str
source: Path
dest: Path # the .html written
delivery: str = "dependency"
notice: str | Path | None = None
rename: dict = field(default_factory=dict)
overrides: dict = field(default_factory=dict) # manifest keys a run file replaces
minify: bool = False
@property
def vendored(self) -> bool:
return self.delivery == "vendored"
@property
def is_vue(self) -> bool:
return (self.source / "app" / "package.json").exists()
@dataclass
class Built:
plexus: Plexus
html: str = ""
hits: list = field(default_factory=list)
notes: list = field(default_factory=list)
def discover() -> list[str]:
return sorted(p.parent.name for p in PLEXUSES.glob("*/plexus.json"))
def resolve(name: str, *, source=None, dest=None, delivery="dependency", notice=None,
rename=None, overrides=None, minify=False, base: Path | None = None) -> Plexus:
"""One plexus, every path absolute. Raises PlexusError saying what is wrong."""
base = base or Path.cwd()
src = Path(source) if source else PLEXUSES / name
src = src if src.is_absolute() else (base / src).resolve()
if not (src / "plexus.json").exists():
raise PlexusError(f"no plexus at {src} (have: {', '.join(discover()) or 'none'})")
if dest:
out = Path(dest) if Path(dest).is_absolute() else (base / dest).resolve()
if out.suffix != ".html":
out = out / "index.html"
else:
out = GEN / name / "index.html"
if delivery not in ("dependency", "vendored"):
raise PlexusError(f"{name}: delivery must be dependency or vendored")
if delivery == "vendored":
if notice is None:
raise PlexusError(f"{name}: a vendored plexus needs `notice` — \"none\", or a file "
f"whose text heads the page. The tool never picks the licensing.")
if notice != "none":
notice = Path(notice) if Path(notice).is_absolute() else (base / notice).resolve()
if not notice.is_file():
raise PlexusError(f"{name}: notice {notice} is not a file")
return Plexus(name=name, source=src, dest=out, delivery=delivery, notice=notice,
rename=dict(rename or {}), overrides=dict(overrides or {}), minify=minify)
def from_runfile(tables: list, base: Path) -> list[Plexus]:
"""The raw [[plexus]] tables of a run file, resolved; every problem at once."""
out, problems = [], []
for i, raw in enumerate(tables):
where = f"plexus[{i}]"
if not isinstance(raw, dict) or not isinstance(raw.get("name"), str):
problems.append(f"{where} needs a name")
continue
where = f"plexus {raw['name']!r}"
unknown = sorted(set(raw) - KEYS)
if unknown:
problems.append(f"{where} has unknown key(s) {', '.join(unknown)} — have: "
f"{', '.join(sorted(KEYS))}")
continue
overrides = {k: raw[k] for k in ("theme", "inline_themes", "title", "description") if k in raw}
try:
out.append(resolve(raw["name"], source=raw.get("source"), dest=raw.get("dest"),
delivery=raw.get("delivery", "dependency"), notice=raw.get("notice"),
rename=raw.get("rename"), overrides=overrides,
minify=bool(raw.get("minify", False)), base=base))
except PlexusError as e:
problems.append(str(e))
if problems:
raise PlexusError("; ".join(problems))
return out
def _manifest(p: Plexus) -> dict:
try:
data = json.loads((p.source / "plexus.json").read_text())
except ValueError as e:
raise PlexusError(f"{p.name}: plexus.json is not valid JSON: {e}") from None
data.update(p.overrides)
return data
def _showcase(p: Plexus) -> dict | None:
"""A plexus may ship showcase.py exposing collect(): what the page shows,
produced at build time by the real tools, so it cannot drift from them."""
collector = p.source / "showcase.py"
if not collector.exists():
return None
try:
spec = importlib.util.spec_from_file_location(f"plexus_{p.name}_showcase", collector)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.collect()
except Exception as e: # noqa: BLE001 - a demo panel must not cost the build
return {"_error": f"{type(e).__name__}: {e}"}
def _graph(name: str, theme: str) -> str:
"""A rendered graph, inlined so the page's CSS can recolour it per theme."""
for candidate in (GRAPHS / f"{name}.{theme}.svg", GRAPHS / f"{name}.svg"):
if candidate.exists():
svg = candidate.read_text()
start = svg.find("<svg")
return svg[start:] if start >= 0 else svg
return "<p>diagram not rendered — run docs/graphs/render.sh</p>"
def theme_css(page: str, themes: list[str]) -> str:
"""The contract and the listed themes, cut to what `page` can use."""
missing = [t for t in themes if not (THEME / "themes" / f"{t}.css").exists()]
if missing:
raise PlexusError(f"no theme {', '.join(map(repr, missing))}")
contract = (THEME / "tokens.css").read_text()
sheets = {t: (THEME / "themes" / f"{t}.css").read_text() for t in themes}
kept_contract, reads = css.shake(contract, page)
kept_themes = {}
for t, text in sheets.items():
kept_themes[t], more = css.shake(text, page)
reads |= more
reads |= set(css.VAR.findall(page))
# Every declaration that could answer a name, from the contract and from
# each theme's own block — the closure runs over all of them.
root = {}
for r in kept_contract:
if r.prelude == ":root" and css.only_properties(r):
root.update(css.declarations(r))
per_theme = {}
for t, kept in kept_themes.items():
per_theme[t] = {}
for r in kept:
if r.prelude.strip() == f'[data-theme="{t}"]' and css.only_properties(r):
per_theme[t].update(css.declarations(r))
everything = {**root, **{k: v for d in per_theme.values() for k, v in d.items()}}
wanted, stack = set(), list(reads)
while stack:
n = stack.pop()
if n in wanted:
continue
wanted.add(n)
if n in everything:
stack.extend(css.VAR.findall(everything[n]))
out = [":root {"] + [f" {k}: {v};" for k, v in root.items() if k in wanted] + ["}"]
for t, decls in per_theme.items():
chosen = [f" {k}: {v};" for k, v in decls.items() if k in wanted]
if chosen:
out += [f'[data-theme="{t}"] {{'] + chosen + ["}"]
for r in kept_contract:
if not css.only_properties(r):
out.append(css.pretty(r))
for t, kept in kept_themes.items():
for r in kept:
if not (r.prelude.strip() == f'[data-theme="{t}"]' and css.only_properties(r)):
out.append(css.pretty(r))
return "\n".join(out) + "\n"
def _finish(built: Built) -> None:
p = built.plexus
if not p.vendored:
return
profile = names.load(p.rename)
built.html = names.rename(built.html, profile)
if isinstance(p.notice, Path):
built.html = f"<!--\n{p.notice.read_text().rstrip()}\n-->\n" + built.html
built.hits = names.scrub(built.html, profile)
def build(p: Plexus) -> Built:
return build_vue(p) if p.is_vue else build_template(p)
def build_template(p: Plexus) -> Built:
built = Built(plexus=p)
manifest = _manifest(p)
template = p.source / "app" / "index.html"
if not template.exists():
raise PlexusError(f"{p.name}: no app/index.html")
theme = manifest.get("theme", "soleprint")
themes = manifest.get("inline_themes") or [theme]
data = {k: v for k, v in manifest.items() if not k.startswith("_")}
showcase = _showcase(p)
if showcase is not None:
data["showcase"] = showcase
today = date.today().isoformat()
page = template.read_text()
for token, value in (
("%%TITLE%%", manifest.get("title", p.name)),
("%%DESCRIPTION%%", manifest.get("description", "")),
("%%DEFAULT_THEME%%", theme),
("%%BUILT%%", f"built {today}" + ("" if p.vendored else " by soleprint")),
("%%GRAPH%%", _graph(manifest.get("graph", "system_overview"), theme)),
("%%BUNDLE%%", json.dumps(data, indent=2)),
):
page = page.replace(token, value)
# Parts, when the template asks for them the way a page does.
if bake.ANCHOR in page:
parts = bake.load_parts()
wanted = bake.parts_used(page, parts)
if wanted:
block = bake.parts_block(wanted, parts, "")
page = page.replace(bake.ANCHOR, bake.ANCHOR + "\n" + block, 1)
built.html = page.replace("%%THEME_CSS%%", theme_css(page, themes))
# Anything else in app/ rides along, for a plexus that outgrows one file.
extras = [x.name for x in (p.source / "app").iterdir() if x.name != "index.html"]
if extras:
built.notes.append(f"also copied beside it: {', '.join(extras)}")
_finish(built)
return built
# ── Vue ──────────────────────────────────────────────────────────────────────
SCRIPT_TAG = re.compile(r'<script type="module"[^>]*\ssrc="([^"]+)"[^>]*></script>')
STYLE_TAG = re.compile(r'<link rel="stylesheet"[^>]*\shref="([^"]+)"[^>]*>')
VITE_CONFIG = """import vue from '@vitejs/plugin-vue'
export default {{
root: {root!r},
base: './',
plugins: [vue()],
cacheDir: {cache!r},
resolve: {{ alias: {{ '@ui': {ui!r} }} }},
build: {{
outDir: {out!r},
emptyOutDir: true,
assetsInlineLimit: 100000000,
cssCodeSplit: false,
modulePreload: false,
minify: {minify},
rollupOptions: {{ output: {{ inlineDynamicImports: true }} }},
}},
}}
"""
def _run(cmd: list, cwd: Path) -> None:
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
if result.returncode != 0:
tail = (result.stdout + result.stderr).strip().splitlines()[-12:]
raise PlexusError(f"`{' '.join(cmd)}` failed:\n " + "\n ".join(tail))
def build_vue(p: Plexus) -> Built:
from . import runfile, slices
built = Built(plexus=p)
manifest = _manifest(p)
app = p.source / "app"
with tempfile.TemporaryDirectory(prefix=f"plexus-{p.name}-") as t:
tmp = Path(t)
work = tmp / "app"
shutil.copytree(app, work, ignore=shutil.ignore_patterns("node_modules", "dist"))
# The app's @ui imports, sliced from common/ui into the build — the
# same closure a consumer would receive, and nothing else.
# The slice follows the plexus's delivery: a vendored page must not
# carry the provenance headers a dependency slice keeps. Its licensing
# is the page's `notice`, stated once, so the inner slice says none.
entry = runfile.Slice(name=p.name, target="vue", delivery=p.delivery,
dest=work / "ui", manifest=tmp / "ui.slice.json",
layout="package", scan=(work / "src",), alias=("@ui",),
theme=manifest.get("theme"), rename=p.rename,
notice="none" if p.vendored else None)
sliced = slices.build(entry)
if sliced.blocked:
raise PlexusError(f"{p.name}: the slice would not build: {'; '.join(sliced.blocked)}")
slices.apply(entry, sliced, slices.plan(entry, sliced), force=True)
built.notes.append(f"slice: {', '.join(sliced.closure)} ({', '.join(sliced.deps)})")
# pnpm 10+ exits non-zero when a dependency's build script is not
# allowed, and every Vite build needs esbuild's. An app that says
# nothing gets the one allowance it cannot build without.
if not (work / "pnpm-workspace.yaml").exists():
(work / "pnpm-workspace.yaml").write_text("allowBuilds:\n esbuild: true\n")
installed = subprocess.run(["pnpm", "install", "--offline"], cwd=work,
capture_output=True, text=True)
if installed.returncode != 0:
_run(["pnpm", "install", "--prefer-offline"], work)
(work / "vite.plexus.config.mjs").write_text(VITE_CONFIG.format(
root=str(work), cache=str(tmp / ".vite"), ui=str(work / "ui" / "src"),
out=str(tmp / "dist"), minify="true" if p.minify else "false"))
_run([str(work / "node_modules" / ".bin" / "vite"), "build",
"--config", "vite.plexus.config.mjs", "--logLevel", "warn"], work)
dist = tmp / "dist"
page = (dist / "index.html").read_text()
def script(m):
code = (dist / m.group(1)).read_text().replace("</script", "<\\/script")
return f'<script type="module">\n{code}\n</script>'
def style(m):
return f"<style>\n{(dist / m.group(1)).read_text()}\n</style>"
page = SCRIPT_TAG.sub(script, page)
page = STYLE_TAG.sub(style, page)
left = re.findall(r'(?:src|href)="\./[^"]+"', page)
if left:
raise PlexusError(f"{p.name}: not inlined: {', '.join(left)}")
title = manifest.get("title", p.name)
page = re.sub(r"<title>.*?</title>", f"<title>{title}</title>", page, count=1, flags=re.S)
data = {k: v for k, v in manifest.items() if not k.startswith("_")}
# The page's own data, for an app that wants it, under a name that says
# what it is to whoever opens the file.
page = page.replace("</head>", f"<script>window.PAGE_DATA = {json.dumps(data)}</script>\n</head>", 1)
built.html = page
_finish(built)
return built
def write(built: Built) -> Path:
p = built.plexus
if built.hits:
raise PlexusError(f"{p.name}: scrub found {len(built.hits)} spr word(s) in a vendored "
f"plexus — nothing written")
p.dest.parent.mkdir(parents=True, exist_ok=True)
p.dest.write_text(built.html)
if not p.is_vue:
for extra in (p.source / "app").iterdir():
if extra.name == "index.html":
continue
target = p.dest.parent / extra.name
if extra.is_dir():
shutil.copytree(extra, target, dirs_exist_ok=True)
else:
shutil.copy2(extra, target)
return p.dest

View File

@@ -19,7 +19,7 @@ import sys
import tempfile
from pathlib import Path
from . import graph, manifest, names, runfile, scan, slices, tokens
from . import css, graph, manifest, names, plexus, runfile, scan, slices, tokens
HERE = Path(__file__).resolve().parent
PASS, FAIL = [], []
@@ -218,6 +218,34 @@ def main() -> int:
"turns the next one into a call)",
chunks and all(c.endswith(";") for c in chunks), [c[-30:] for c in chunks])
section("the plexus")
page = '<div class="card"><h2>x</h2></div><script>el.className = "dyn"</script>'
kept, _ = css.shake('.card{a:1} .gone{b:2} h2{c:3} .dyn{d:4} @keyframes spin{} '
'.card{animation: spin 1s}', page)
preludes = [r.prelude for r in kept]
check("a rule whose class never appears is dropped", ".gone" not in preludes, preludes)
check("a class named only in script still counts", ".dyn" in preludes, preludes)
check("element rules stay; referenced keyframes stay",
"h2" in preludes and "@keyframes spin" in preludes, preludes)
try:
plexus.resolve("bundle", delivery="vendored")
check("(design) a vendored plexus without notice is refused", False)
except plexus.PlexusError as e:
check("(design) a vendored plexus without notice is refused", "notice" in str(e))
b = plexus.resolve("bundle")
check("a plexus needs nothing built first: it lands in gen/plexuses/",
b.dest == plexus.GEN / "bundle" / "index.html")
built = plexus.build(b)
check("every placeholder is filled", not re.search(r"%%[A-Z_]+%%", built.html))
check("each theme it can switch to is carried, and nothing it cannot use",
all(f'[data-theme="{t}"]' in built.html for t in ("lucid", "soleprint", "mcrn"))
and "#spr-theme-toggle" not in built.html and ".prompt-list" not in built.html)
vendored = plexus.build(plexus.resolve("bundle", delivery="vendored", notice="none"))
check("(design) spr's own showcase cannot be vendored — its content is spr",
bool(vendored.hits))
check("no plexus code is left in build.py",
"plexus" not in (manifest.REPO / "build.py").read_text().lower())
section("consumers (read-only, skipped when absent)")
meetus = Path.home() / "wdir/semester/mts/ui/meetus-app"
if meetus.exists():

View File

@@ -355,7 +355,11 @@ def artery_index(request: Request):
veins = load_data("veins.json")
pulses = load_data("pulses.json")
shunts_data = load_data("shunts.json")
plexuses = load_data("plexuses.json")
# A plexus is built on its own (`make dist`), so the hub lists what is on
# disk when no data file names them — the same fallback shunts use below.
plexuses = load_data("plexuses.json") or scan_directory(SPR_ROOT / "artery" / "plexuses")
for p in plexuses:
p.setdefault("status", "ready")
# Scan directories for shunts not in data files
shunts = (