Explorar o código

Add the web-stack project pages: astralis, statum, spry

Three project detail pages on A3's docs template: masthead with
Developer current, breadcrumb, docs-header with the facts plate
(version from MANIFEST.usm, LGPL-3.0-or-later, usm install name,
Vala), the sticky docs-toc + docs-prose layout with explicit grid
placement, the five stable section ids, a docs-siblings strip, the
docs-pager in A2's catalogue order and A2's footer.

All code samples carry provenance figcaptions naming the source
file they were lifted from:

- astralis: mined from the seventeen runnable examples (SimpleApi,
  PathRouting, JsonApi, HeadersAndCookies, FormData, SseExample,
  DocumentBuilder, FastResources, FileServer, WebConfigExample,
  ErrorHandling) since the repo ships no README; the src/ layering
  (Core/Server/Components/Endpoints/Data/Markup) is described from
  meson.build. Notes honestly that the typelib now installs as
  astralis-0.1.typelib, the namespace-mismatch fix.
- statum: mined from getting-started.md, model.md references,
  example/Main.vala and tests/TestMain.vala (the int64/bool
  serialisation round-trip is quoted from the suite).
- spry: mined from its README (stack diagram, auth slot field
  table, guard semantics, shipped actions, UserService contract,
  CLI surface, deploy) and src/Actions/LoginAction.vala. Records
  the honest typelib gap: the GIR ships but valac 0.56 cannot
  compile a typelib for cross-namespace nested parents.

check.py: astralis, statum and spry removed from UPCOMING_PAGES so
the checker enforces them; locksmith/vallama/usm stay allowlisted
for the parallel A5 task.

Verified: python3 check.py . green (10 pages, 312 references);
headless CDP render of all three pages - prefers-color-scheme
flips verified via Emulation.setEmulatedMedia (100% of pixels
differ between light and dark screenshots), docs-toc and
docs-prose land on the same grid row with prose left of toc at
1280px, and no element outside <pre> overflows horizontally at
390, 768 or 1280px (narrow-width single-column stacking matches
the A3 exemplar pages).
saddle-ClankerSaddle hai 2 semanas
pai
achega
c0a7533c49
Modificáronse 4 ficheiros con 978 adicións e 6 borrados
  1. 3 6
      check.py
  2. 337 0
      developer/astralis/index.html
  3. 316 0
      developer/spry/index.html
  4. 322 0
      developer/statum/index.html

+ 3 - 6
check.py

@@ -26,13 +26,10 @@ EXCLUDE_DIRS = {"Original", "Proposed", ".git"}
 # Site-root-relative pages that are linked ahead of their existence because a
 # later task in this goal builds them. Remove entries here once the page lands
 # so the checker enforces them again.
-# TODO(A4/A5): the six remaining detail pages land in wave 3; strip these
-# entries as each wave merges. The five Invercargill-family pages (A3) have
-# landed and are enforced.
+# A5 (locksmith, vallama, usm) runs in parallel with A4; the goal agent strips
+# the remaining entries at merge. The web-stack pages (astralis, statum, spry)
+# have landed and are enforced.
 UPCOMING_PAGES = {
-    "developer/astralis/",
-    "developer/statum/",
-    "developer/spry/",
     "developer/locksmith/",
     "developer/vallama/",
     "developer/usm/",

+ 337 - 0
developer/astralis/index.html

@@ -0,0 +1,337 @@
+<!doctype html>
+<html lang="en-nz">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Astralis — Astrologue Developer</title>
+  <meta name="description" content="Astralis is the library for writing HTTP backends in Vala — routing, headers, cookies, form data, server-sent events, document markup and layered web-config.json settings, on Inversion and Invercargill. Free software, LGPL-3.0-or-later.">
+  <meta name="color-scheme" content="light dark">
+  <meta name="theme-color" content="#f2ebda" media="(prefers-color-scheme: light)">
+  <meta name="theme-color" content="#0a0a12" media="(prefers-color-scheme: dark)">
+  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='27' fill='none' stroke='%23a8801f' stroke-width='4'/%3E%3Cpath d='M32 8l6 18 18 6-18 6-6 18-6-18-18-6 18-6z' fill='%23a8801f'/%3E%3Ccircle cx='32' cy='32' r='4' fill='%23003078'/%3E%3C/svg%3E">
+  <meta property="og:title" content="Astralis — Astrologue Developer">
+  <meta property="og:description" content="The library for writing HTTP backends in Vala: endpoints and routing, headers and cookies, form data, server-sent events, a document builder — and a web-config.json settings layer.">
+  <meta property="og:type" content="website">
+  <link rel="stylesheet" href="../../assets/css/style.css">
+</head>
+<body>
+  <a class="skip-link" href="#main">Skip to content</a>
+
+  <header class="masthead">
+    <div class="container masthead__inner">
+      <a class="brand" href="../../">
+        <img class="brand__logo brand__logo--light" src="../../assets/img/astrologue-light.png" width="392" height="89" alt="Astrologue">
+        <img class="brand__logo brand__logo--dark" src="../../assets/img/astrologue-dark.png" width="392" height="89" alt="Astrologue">
+      </a>
+      <nav class="site-nav" aria-label="Primary">
+        <ul class="site-nav__list">
+          <li><a href="../../">Home</a></li>
+          <li><a href="../../#news">News</a></li>
+          <li><a href="../../#download">Download</a></li>
+          <li><a href="../" aria-current="page">Developer</a></li>
+          <li><a class="nav-cta" href="../../#download">Get Astrologue</a></li>
+        </ul>
+      </nav>
+    </div>
+  </header>
+
+  <main id="main">
+
+    <nav class="breadcrumb" aria-label="Breadcrumb">
+      <div class="container">
+        <ol>
+          <li><a href="../../">Astrologue</a></li>
+          <li><a href="../">Developer</a></li>
+          <li aria-current="page">Astralis</li>
+        </ol>
+      </div>
+    </nav>
+
+    <section class="docs-header">
+      <div class="container">
+        <p class="kicker"><span>Web stack · Where a request lands</span></p>
+        <h1>Astralis</h1>
+        <p class="lede">The library for writing HTTP backends in Vala. An endpoint is a class, a route is metadata, a response is a value — and beneath it sit header and cookie handling, form parsing, server-sent events, a document builder and a layered JSON configuration system, all wired through Inversion and written with Invercargill.</p>
+        <dl class="docs-facts">
+          <div><dt>Version</dt> <dd><code>0.1</code></dd></div>
+          <div><dt>Licence</dt> <dd>LGPL-3.0-or-later</dd></div>
+          <div><dt>Package</dt> <dd><code>usm&nbsp;install&nbsp;astralis</code></dd></div>
+          <div><dt>Language</dt> <dd>Vala</dd></div>
+        </dl>
+      </div>
+    </section>
+
+    <section class="band band--plain" id="docs">
+      <div class="container docs-layout">
+
+        <nav class="docs-toc" aria-label="On this page">
+          <h2>On this page</h2>
+          <ul>
+            <li><a href="#overview">Overview</a></li>
+            <li><a href="#features">Features</a></li>
+            <li><a href="#installation">Installation</a></li>
+            <li><a href="#usage">Usage &amp; documentation</a></li>
+            <li><a href="#package">Package &amp; source</a></li>
+          </ul>
+        </nav>
+
+        <div class="docs-prose">
+
+          <header class="section-head" id="overview">
+            <p class="kicker"><span>Overview</span></p>
+            <h2>The server the stack stands on</h2>
+          </header>
+          <p>Astralis takes an HTTP request from the socket to your code and back. It is built on GNU libmicrohttpd — driven in epoll mode from the GLib main loop, with suspend/resume so slow clients do not occupy a thread — and everything above that is Vala: routing by endpoint class, a pipeline of request values, results as values rather than writes to a socket.</p>
+          <p>The design is deliberately small. You implement the <code>Endpoint</code> interface, one async method that receives an <code>HttpContext</code> and returns an <code>HttpResult</code>:</p>
+          <figure class="code-plate">
+            <pre><code>class HelloEndpoint : Object, Endpoint {
+    public async HttpResult handle_request(HttpContext http_context, RouteContext route) throws Error {
+        return new HttpStringResult("Hello from Astralis!");
+    }
+}
+
+void main() {
+    var application = new WebApplication(8080);
+    application.add_endpoint&lt;HelloEndpoint&gt;(new EndpointRoute("/hello"));
+    application.run();
+}</code></pre>
+            <figcaption>From <code>examples/SimpleApi.vala</code> — a complete server.</figcaption>
+          </figure>
+          <p>Routes can carry named segments, resolved for you into the route context:</p>
+          <figure class="code-plate">
+            <pre><code>class HelloNameEndpoint : Object, Endpoint {
+    public async HttpResult handle_request(HttpContext http_context, RouteContext route_info) throws Error {
+        string? name = null;
+        route_info.mapped_parameters.try_get("name", out name);
+        return new HttpStringResult(@"Hello, $(name ?? "Unknown")!");
+    }
+}
+
+// registered as:
+application.add_endpoint&lt;HelloNameEndpoint&gt;(new EndpointRoute("/hello/{name}"));</code></pre>
+            <figcaption>From <code>examples/PathRouting.vala</code> — the router's named-segment feature.</figcaption>
+          </figure>
+          <p>Endpoints are registered into the <a href="../inversion/">Inversion</a> container — scoped by default, so one instance serves one request — and the stack above builds on exactly this: <a href="../statum/">Statum</a>'s pages, actions and channels are Astralis endpoints, and <a href="../spry/">Spry</a>'s authentication rides inside them.</p>
+
+          <header class="section-head" id="features">
+            <p class="kicker"><span>Features</span></p>
+            <h2>What it hands you</h2>
+          </header>
+          <ul class="docs-features">
+            <li><strong>Endpoints and routing.</strong> Classes with route metadata; named segments like <code>/users/{id}/posts</code> arrive pre-parsed in the route context.</li>
+            <li><strong>Headers and cookies.</strong> Request headers and cookies as enumerable collections; response headers set fluently on the result value.</li>
+            <li><strong>Form data.</strong> An async <code>FormDataParser</code> yields fields and files with typed getters and defaults for absent fields.</li>
+            <li><strong>Server-sent events.</strong> <code>SseEndpoint</code> subclasses broadcast to every open stream, greet new connections, and set a retry interval.</li>
+            <li><strong>A document builder.</strong> <code>MarkupDocument</code> parses, builds and queries markup with XPath selectors — HTML without a template engine.</li>
+            <li><strong>Response compression.</strong> gzip, brotli and zstd compressors with encoding negotiation; <code>FastResource</code> serves pre-loaded content straight from memory.</li>
+            <li><strong>Layered configuration.</strong> A <code>web-config.json</code> system with per-environment overlays merged over the base file.</li>
+          </ul>
+
+          <header class="section-head" id="installation">
+            <p class="kicker"><span>Installation</span></p>
+            <h2>Getting the library</h2>
+          </header>
+          <p>With USM on your machine, one command acquires, builds and installs the library together with its C header, Vala binding and introspection:</p>
+          <figure class="code-plate">
+            <pre><code>usm install astralis</code></pre>
+            <figcaption>Installs from the Astrologue package repository.</figcaption>
+          </figure>
+          <p>From source, the build lives at the repository root:</p>
+          <figure class="code-plate">
+            <pre><code>git clone &lt;the astralis repository&gt;
+cd astralis
+meson setup . build
+ninja -C build
+ninja -C build install</code></pre>
+            <figcaption>The same meson invocation the package's own build script uses.</figcaption>
+          </figure>
+          <p>Then compile against it with Vala's usual package mechanism:</p>
+          <figure class="code-plate">
+            <pre><code>valac --pkg astralis-0.1 my-server.vala</code></pre>
+            <figcaption>The installed <code>astralis-0.1.deps</code> resolves the Invercargill and Inversion bindings for you.</figcaption>
+          </figure>
+
+          <header class="section-head" id="usage">
+            <p class="kicker"><span>Usage &amp; documentation</span></p>
+            <h2>Working with requests</h2>
+          </header>
+          <p>Astralis has no README; its documentation is its examples. Seventeen runnable programs cover the API surface, and each idiom below is lifted from them.</p>
+
+          <h3>Responses are values</h3>
+          <p>An <code>HttpResult</code> carries body, status and headers together, so handlers stay pure and testable — headers chain onto the constructor:</p>
+          <figure class="code-plate">
+            <pre><code>return new HttpStringResult(user.to_json())
+    .set_header("Content-Type", "application/json")
+    .set_header("Access-Control-Allow-Origin", "*");</code></pre>
+            <figcaption>From <code>examples/JsonApi.vala</code> and <code>PathRouting.vala</code>.</figcaption>
+          </figure>
+          <p>Failures use the same vocabulary — a helper returns the status you name:</p>
+          <figure class="code-plate">
+            <pre><code>return error_response(@"Resource with ID $id not found", StatusCode.NOT_FOUND);</code></pre>
+            <figcaption>From <code>examples/ErrorHandling.vala</code>.</figcaption>
+          </figure>
+
+          <h3>Headers, cookies and queries</h3>
+          <p>Request headers and cookies arrive as Invercargill collections, so the whole Enumerable toolkit applies — count, iterate, filter — with single-field getters alongside:</p>
+          <figure class="code-plate">
+            <pre><code>var value = http_context.request.get_header(header_name);
+
+http_context.request.cookies.to_immutable_buffer()
+    .iterate(kv =&gt; parts.add(@"$(kv.key): $(kv.value)"));
+
+if (http_context.request.cookies.to_immutable_buffer().count() == 0) {
+    parts.add("(No cookies sent)");
+}</code></pre>
+            <figcaption>From <code>examples/HeadersAndCookies.vala</code>.</figcaption>
+          </figure>
+          <p>Form bodies parse asynchronously, with getters that take defaults for absent fields:</p>
+          <figure class="code-plate">
+            <pre><code>FormData form_data = yield FormDataParser.parse(
+    http_context.request, upload_path);
+
+var name    = form_data.get_field_or_default("name", "Anonymous");
+var email   = form_data.get_field_or_default("email", "no-email@example.com");
+var country = form_data.get_field_or_default("country", "us");</code></pre>
+            <figcaption>From <code>examples/FormData.vala</code> — fields and uploaded files arrive together.</figcaption>
+          </figure>
+
+          <h3>Server-sent events</h3>
+          <p><code>SseEndpoint</code> is the base class for push. A singleton instance holds shared state; <code>new_connection</code> greets each client and <code>broadcast_event</code> reaches every open stream:</p>
+          <figure class="code-plate">
+            <pre><code>public class CounterEndpoint : SseEndpoint {
+    private int counter = 0;
+    private Mutex counter_mutex = Mutex();
+
+    public override uint retry_interval { get { return 2000; } }
+
+    public override async void new_connection(HttpContext http_context,
+            RouteContext route_context, SseStream stream) {
+        yield stream.send_event(
+            new SseEvent.with_type("counter", counter.to_string()));
+    }
+
+    public async void increment() {
+        counter++;
+        yield broadcast_event(
+            new SseEvent.with_type("counter", counter.to_string()));
+    }
+}</code></pre>
+            <figcaption>From <code>examples/SseExample.vala</code> — a mutex-guarded counter broadcast to all tabs.</figcaption>
+          </figure>
+
+          <h3>Building documents</h3>
+          <p>When a handler must produce HTML rather than JSON, the markup subsystem parses, edits and queries documents without string templates:</p>
+          <figure class="code-plate">
+            <pre><code>var doc = new MarkupDocument.from_string(page_html);
+
+var header_card = body.append_child_element("div");
+header_card.append_child_with_text("h1", "A heading");
+var toggle_form = item_div.append_child_element("form");
+var toggle_btn = toggle_form.append_child_element("button");</code></pre>
+            <figcaption>From <code>examples/DocumentBuilder.vala</code> — elements are built by appending children; XPath selectors find them again.</figcaption>
+          </figure>
+          <p><code>MarkupTemplate</code> composes documents, and XPath results come back as lazy Enumerables over the tree.</p>
+
+          <h3>Static content and compression</h3>
+          <p><code>FastResource</code> holds content pre-loaded in memory — the example notes it should be registered as a startup or singleton since it keeps its bytes for the process lifetime:</p>
+          <figure class="code-plate">
+            <pre><code>FastResource.from_string(content)   // load from a string
+FastResource.from_byte_array(bytes) // load from bytes
+FastResource(path)                  // load from the filesystem</code></pre>
+            <figcaption>From <code>examples/FastResources.vala</code> — the three constructors.</figcaption>
+          </figure>
+          <p><code>FilesystemResource</code> serves a directory tree, and the compressor components negotiate gzip, brotli and zstd against what the client accepts — the file-server example announces exactly that:</p>
+          <figure class="code-plate">
+            <pre><code>Available compression: gzip, brotli, zstd
+
+Try:
+  curl http://localhost:$port/
+  curl --compressed http://localhost:$port/style.css</code></pre>
+            <figcaption>From <code>examples/FileServer.vala</code>, a complete static server in 141 lines.</figcaption>
+          </figure>
+
+          <h3>Configuration</h3>
+          <p>Settings live in a <code>web-config.json</code> at the application root, and an environment overlay — <code>web-config.staging.json</code>, selected with <code>ASTRALIS_ENV=staging</code> — merges over it. Inside a handler, the configuration is injected through Inversion and read with typed getters:</p>
+          <figure class="code-plate">
+            <pre><code>var config = inject&lt;WebConfig&gt;();
+
+var port     = config.get_int("port", 8080);
+var app_name = config.get_string("app_name", "Unknown App");
+var debug    = config.get_bool("debug", false);</code></pre>
+            <figcaption>From <code>examples/WebConfigExample.vala</code> — every getter carries a default.</figcaption>
+          </figure>
+
+          <h3>Inside the box</h3>
+          <p>The source tree names its own layers: <strong>Core</strong> holds <code>WebApplication</code>, the pipeline, <code>HttpContext</code> and the <code>WebConfig</code> family; <strong>Server</strong> holds the libmicrohttpd integration and socket monitor; <strong>Components</strong> holds routing and the three compressors; <strong>Endpoints</strong> holds the endpoint contracts and resource servers; <strong>Data</strong> holds form parsing; and <strong>Markup</strong> holds the document builder. The examples directory doubles as a guided tour of each.</p>
+
+          <header class="section-head" id="package">
+            <p class="kicker"><span>Package &amp; source</span></p>
+            <h2>The ledger</h2>
+          </header>
+          <dl class="docs-package">
+            <div><dt>Version</dt><dd><code>0.1</code> (from <code>MANIFEST.usm</code>)</dd></div>
+            <div><dt>Licence</dt><dd>LGPL-3.0-or-later — free to run, study, share and improve; the LGPL keeps it free to link from any program.</dd></div>
+            <div><dt>Package</dt><dd><code>usm install astralis</code></dd></div>
+            <div><dt>Provides</dt><dd><code>libastralis-0.1.so</code>, <code>astralis-0.1.h</code>, <code>astralis-0.1.vapi</code> + <code>.deps</code>, <code>astralis-0.1.gir</code>, <code>astralis-0.1.typelib</code>, <code>astralis-0.1.pc</code></dd></div>
+            <div><dt>Needs at runtime</dt><dd>GLib and GObject, libmicrohttpd, json-glib, libxml2, zlib, brotli and zstd encoders, plus the stack beneath: <code>libinvercargill-1.so</code> and <code>libinversion-0.1.so</code>.</dd></div>
+            <div><dt>Source</dt><dd>The repository <code>Tilo15/astralis</code> on the Astrologue forge (git over ssh, port 222). Free software: the licence guarantees your right to the source.</dd></div>
+          </dl>
+          <p>The typelib ships as <code>astralis-0.1.typelib</code>, matching the GIR's namespace — an earlier package installed it under a lib-prefixed name that introspection consumers could never resolve; the mismatch is fixed and the manifest now names the file the build actually produces.</p>
+
+          <nav class="docs-siblings" aria-label="Built on Astralis">
+            <p>The libraries that stand on it directly:</p>
+            <ul>
+              <li><a href="../statum/">Statum</a></li>
+              <li><a href="../spry/">Spry</a></li>
+            </ul>
+          </nav>
+
+          <nav class="docs-pager" aria-label="Project pages">
+            <a class="docs-pager__link docs-pager__link--prev" href="../invercargill-sql-inversion/">
+              <span>Previous</span>
+              <strong>Invercargill-Sql-Inversion</strong>
+            </a>
+            <p class="docs-pager__up"><a href="../">All eleven</a></p>
+            <a class="docs-pager__link docs-pager__link--next" href="../statum/">
+              <span>Next in the catalogue</span>
+              <strong>Statum</strong>
+            </a>
+          </nav>
+
+        </div>
+      </div>
+    </section>
+
+  </main>
+
+  <footer class="site-footer">
+    <div class="container">
+      <div class="footer-grid">
+        <div class="foot-brand">
+          <img class="brand__logo brand__logo--dark" src="../../assets/img/astrologue-dark.png" width="392" height="89" alt="Astrologue">
+          <p>An operating system for change — kept for self sufficiency, resilience, and software freedom, so regular people can hold the power of general purpose computers in their own hands.</p>
+        </div>
+        <nav aria-label="The four freedoms">
+          <p class="foot-title">The Four Freedoms</p>
+          <ol class="freedoms">
+            <li><strong>Run</strong> the software as you wish, for any purpose.</li>
+            <li><strong>Study</strong> the source and change it to do your computing as you wish.</li>
+            <li><strong>Share</strong> copies so you can help others.</li>
+            <li><strong>Modify</strong> and publish your versions so everyone benefits.</li>
+          </ol>
+        </nav>
+        <nav aria-label="Site">
+          <p class="foot-title">Site</p>
+          <ul class="foot-list">
+            <li><a href="../../">Home</a></li>
+            <li><a href="../../#news">News</a></li>
+            <li><a href="../../#download">Download</a></li>
+            <li><a href="../">Developer</a></li>
+          </ul>
+        </nav>
+      </div>
+      <p class="colophon">Set in Cinzel and EB Garamond (SIL Open Font License). Astrologue is free software, licensed GPL-3.0-or-later: take it, tend it, pass it on. The libraries are LGPL-3.0-or-later, so they remain free to link.</p>
+    </div>
+  </footer>
+</body>
+</html>

+ 316 - 0
developer/spry/index.html

@@ -0,0 +1,316 @@
+<!doctype html>
+<html lang="en-nz">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Spry — Astrologue Developer</title>
+  <meta name="description" content="Spry is the authentication and user-management layer for Statum applications — the signed auth slot, authoritative guards, subclassable actions, the spry_users schema — plus the spry CLI that scaffolds, keys and deploys an application. Free software, LGPL-3.0-or-later.">
+  <meta name="color-scheme" content="light dark">
+  <meta name="theme-color" content="#f2ebda" media="(prefers-color-scheme: light)">
+  <meta name="theme-color" content="#0a0a12" media="(prefers-color-scheme: dark)">
+  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='27' fill='none' stroke='%23a8801f' stroke-width='4'/%3E%3Cpath d='M32 8l6 18 18 6-18 6-6 18-6-18-18-6 18-6z' fill='%23a8801f'/%3E%3Ccircle cx='32' cy='32' r='4' fill='%23003078'/%3E%3C/svg%3E">
+  <meta property="og:title" content="Spry — Astrologue Developer">
+  <meta property="og:description" content="Reusable authentication and user-management services for Statum — the signed auth slot, guards and subclassable actions — plus the spry CLI that scaffolds, keys and deploys an application.">
+  <meta property="og:type" content="website">
+  <link rel="stylesheet" href="../../assets/css/style.css">
+</head>
+<body>
+  <a class="skip-link" href="#main">Skip to content</a>
+
+  <header class="masthead">
+    <div class="container masthead__inner">
+      <a class="brand" href="../../">
+        <img class="brand__logo brand__logo--light" src="../../assets/img/astrologue-light.png" width="392" height="89" alt="Astrologue">
+        <img class="brand__logo brand__logo--dark" src="../../assets/img/astrologue-dark.png" width="392" height="89" alt="Astrologue">
+      </a>
+      <nav class="site-nav" aria-label="Primary">
+        <ul class="site-nav__list">
+          <li><a href="../../">Home</a></li>
+          <li><a href="../../#news">News</a></li>
+          <li><a href="../../#download">Download</a></li>
+          <li><a href="../" aria-current="page">Developer</a></li>
+          <li><a class="nav-cta" href="../../#download">Get Astrologue</a></li>
+        </ul>
+      </nav>
+    </div>
+  </header>
+
+  <main id="main">
+
+    <nav class="breadcrumb" aria-label="Breadcrumb">
+      <div class="container">
+        <ol>
+          <li><a href="../../">Astrologue</a></li>
+          <li><a href="../">Developer</a></li>
+          <li aria-current="page">Spry</li>
+        </ol>
+      </div>
+    </nav>
+
+    <section class="docs-header">
+      <div class="container">
+        <p class="kicker"><span>Web stack · Users at the door</span></p>
+        <h1>Spry</h1>
+        <p class="lede">Authentication and user management for Statum applications. Identity is a signed Statum slot — no cookies, no bearer tokens — with user ids sealed in encrypted private data, guards that re-validate against the store on every request, and a set of ready-made actions built for subclassing. The spry CLI scaffolds an application, maintains its keys and ships it as a container image.</p>
+        <dl class="docs-facts">
+          <div><dt>Version</dt> <dd><code>0.2</code></dd></div>
+          <div><dt>Licence</dt> <dd>LGPL-3.0-or-later</dd></div>
+          <div><dt>Package</dt> <dd><code>usm&nbsp;install&nbsp;spry</code></dd></div>
+          <div><dt>Language</dt> <dd>Vala</dd></div>
+        </dl>
+      </div>
+    </section>
+
+    <section class="band band--plain" id="docs">
+      <div class="container docs-layout">
+
+        <nav class="docs-toc" aria-label="On this page">
+          <h2>On this page</h2>
+          <ul>
+            <li><a href="#overview">Overview</a></li>
+            <li><a href="#features">Features</a></li>
+            <li><a href="#installation">Installation</a></li>
+            <li><a href="#usage">Usage &amp; documentation</a></li>
+            <li><a href="#package">Package &amp; source</a></li>
+          </ul>
+        </nav>
+
+        <div class="docs-prose">
+
+          <header class="section-head" id="overview">
+            <p class="kicker"><span>Overview</span></p>
+            <h2>Who is asking?</h2>
+          </header>
+          <p>Spry answers the first question every application with users must answer, and it answers with the machinery <a href="../statum/">Statum</a> already provides rather than adding a parallel one. It renders nothing itself — pages, layouts and resources are Statum's job, and the auth UI arrives as app-owned templates generated by the CLI. Its place in the stack:</p>
+          <figure class="code-plate">
+            <pre><code>astralis                     HTTP request handling
+└─ Statum                    pages, slots/state, directives, client JS
+   └─ Spry                   THIS LIBRARY: users, auth slot, actions, guards
+      ├─ Invercargill-Sql    spry_users / spry_user_permissions schema
+      └─ Inversion           module + service wiring</code></pre>
+            <figcaption>The stack, from Spry's README.</figcaption>
+          </figure>
+          <p>Adding it to an application is one module; seeding an administrator at startup is idempotent, so the same code runs on every boot:</p>
+          <figure class="code-plate">
+            <pre><code>application.add_module&lt;StatumModule&gt;();
+application.add_module&lt;SpryModule&gt;();      // UserService + schema + migrations
+
+var statum = application.configure_with&lt;StatumConfigurator&gt;();
+SpryAuth.register_actions(statum);          // or hand-pick with statum.action&lt;T&gt;()
+
+var scope = application.container.create_scope();
+yield SpryAuth.ensure_admin(scope.resolve&lt;UserService&gt;(),
+    "admin", "admin@example.com", "change-me");</code></pre>
+            <figcaption>From Spry's README — <code>SpryModule</code> registers no endpoints; it wires the migration, entity mappings and the scoped <code>UserService</code>.</figcaption>
+          </figure>
+
+          <header class="section-head" id="features">
+            <p class="kicker"><span>Features</span></p>
+            <h2>What it hands you</h2>
+          </header>
+          <ul class="docs-features">
+            <li><strong>The signed auth slot.</strong> Identity as a SESSION slot of type name <code>"auth"</code> — no cookies, no bearer tokens — written once at login and read on every request.</li>
+            <li><strong>Encrypted user ids.</strong> The authoritative <code>user_id</code> lives in the slot's encrypted private data and never reaches the browser; a stolen public copy cannot impersonate anyone.</li>
+            <li><strong>Authoritative guards.</strong> <code>SpryAuth.require_login</code> and <code>require_permission</code> re-validate against the store on every guarded request — disabling, deleting or revoking takes effect immediately, and a store failure fails closed.</li>
+            <li><strong>Subclassable actions.</strong> Nine shipped actions — login, register, logout, password change, enable, grant, revoke, alter, delete — whose redirect URIs and notification messages are <code>protected virtual</code> properties.</li>
+            <li><strong>Sealed row actions.</strong> User-management actions read their target id from encrypted private data authored per row, so management buttons cannot be aimed at another account.</li>
+            <li><strong>A user store.</strong> <code>UserService</code> with idempotent permission grants, targeted revokes, and an authenticate path with no account-enumeration side channel.</li>
+            <li><strong>The spry CLI.</strong> <code>spry new</code> scaffolds a packaged application, <code>spry add</code> grows it, <code>spry keys</code> maintains key material, <code>spry dev</code> is the watch loop, <code>spry deploy</code> builds the container image.</li>
+          </ul>
+
+          <header class="section-head" id="installation">
+            <p class="kicker"><span>Installation</span></p>
+            <h2>Getting the library</h2>
+          </header>
+          <p>With USM on your machine, one command acquires, builds and installs the library, the spry CLI, the C header, Vala binding and introspection:</p>
+          <figure class="code-plate">
+            <pre><code>usm install spry</code></pre>
+            <figcaption>Installs from the Astrologue package repository.</figcaption>
+          </figure>
+          <p>From source, the build lives at the repository root:</p>
+          <figure class="code-plate">
+            <pre><code>git clone &lt;the Spry repository&gt;
+cd Spry
+meson setup . build
+ninja -C build
+ninja -C build install</code></pre>
+            <figcaption>The same meson invocation the package's own build script uses.</figcaption>
+          </figure>
+          <p>Then compile against it with Vala's usual package mechanism:</p>
+          <figure class="code-plate">
+            <pre><code>valac --pkg spry-0.2 my-app.vala</code></pre>
+            <figcaption>The installed <code>spry-0.2.deps</code> resolves the Statum, Astralis, Invercargill-Sql and Inversion bindings for you.</figcaption>
+          </figure>
+
+          <header class="section-head" id="usage">
+            <p class="kicker"><span>Usage &amp; documentation</span></p>
+            <h2>Identity, guards, actions</h2>
+          </header>
+          <p>Spry's README is its documentation, and it is a thorough one — the stack diagram, every action's form fields and guards, the user store's contract, and the CLI's full command surface. These idioms are lifted from it.</p>
+
+          <h3>The auth slot</h3>
+          <p>The slot's public fields are signed and browser-visible — bind them for navigation and guards. The private field is signed and encrypted, and never reaches the browser:</p>
+          <figure class="code-plate">
+            <pre><code>username       public   signed    login name; bind for nav/guards
+display_name   public   signed    forename + surname, falling back to username
+permissions    public   signed    string[] patterns: admin/* match everything
+logout         public   signed    embedded ActionDto for logout buttons
+user_id        private  signed +  authoritative spry_users.id — never
+                        encrypted reaches the browser</code></pre>
+            <figcaption>The <code>auth</code> slot, field by field, from Spry's README.</figcaption>
+          </figure>
+          <p>Because the whole slot is signed under static keys in <code>web-config.json</code>, a session survives a server restart; because <code>user_id</code> is sealed, editing a browser-side copy gains nothing.</p>
+
+          <h3>Reading identity, guarding handlers</h3>
+          <figure class="code-plate">
+            <pre><code>public class ProfileEntrypoint : StatumEntrypoint {
+
+    protected UserService users = inject&lt;UserService&gt;();
+
+    public override async DirectiveBuilder handle() throws GLib.Error {
+        var guard = yield SpryAuth.require_login(directives(), request, users);
+        if (guard != null) {
+            return (!)guard;                       // navigate("/login")
+        }
+
+        var me = SpryAuth.current(request);
+        if (!me.has_permission("reports.read")) {  // admin / users.* wildcards work
+            ...
+        }
+        ...
+    }
+}</code></pre>
+            <figcaption>From Spry's README — the guard takes the caller's builder and the caller's <code>UserService</code>.</figcaption>
+          </figure>
+          <p><code>SpryAuth.current</code> returns the cheap display identity — the signed snapshot minted at login, anonymous when no slot is held — while the guards are authoritative: <code>require_login</code> answers <code>null</code> only when the held slot's user id still maps to a live, enabled account, and <code>require_permission</code> additionally re-derives the permission set from the store. Permission patterns keep their exact older semantics: <code>admin</code> and <code>*</code> match everything, <code>prefix*</code> matches any permission with that prefix, everything else is exact.</p>
+
+          <h3>The shipped actions</h3>
+          <p>Nine actions cover the standard account surface. Each is a <code>StatumAction</code> subclass designed for subclassing — a different landing page or friendlier message is one override away:</p>
+          <figure class="code-plate">
+            <pre><code>public class LoginAction : StatumAction {
+
+    private UserService users = inject&lt;UserService&gt;();
+
+    /** URI navigated to after a successful login. */
+    protected virtual string landing_uri { get { return "/"; } }
+
+    /** Notification shown when the credentials do not match an account. */
+    protected virtual string invalid_credentials_message {
+        get { return "Invalid username or password"; }
+    }
+
+    public override async DirectiveBuilder handle() throws GLib.Error {
+        ...
+        var user = yield users.authenticate_user(username, password);
+        if (user == null) {
+            return directives().notify("error", invalid_credentials_message);
+        }
+
+        return SpryAuth.login(directives(), (!)user, action_registry)
+            .navigate(landing_uri);
+    }
+}</code></pre>
+            <figcaption>From <code>src/Actions/LoginAction.vala</code> — unknown usernames and wrong passwords answer the same generic notify, so the response leaks no account existence.</figcaption>
+          </figure>
+          <p>The full set: <code>LoginAction</code>, <code>RegisterAction</code>, <code>LogoutAction</code>, <code>ChangePasswordAction</code> (logged-in), and — guarded by <code>require_permission</code>, default <code>admin</code> — <code>SetUserEnabledAction</code>, <code>GrantPermissionAction</code>, <code>RevokePermissionAction</code>, <code>AlterUserAction</code> (refuses self-alter) and <code>DeleteUserAction</code> (refuses self-delete). The five management actions read their target from sealed private data:</p>
+          <figure class="code-plate">
+            <pre><code>var target = new Spry.Actions.UserActionPrivate();
+target.user_id = row_user.id;
+row.grant = action_registry
+    .author_private&lt;Spry.Actions.GrantPermissionAction,
+                    Spry.Actions.UserActionPrivate&gt;(target);</code></pre>
+            <figcaption>From Spry's README — the id travels in an encrypted blob sealed under the action's own namespace.</figcaption>
+          </figure>
+          <p>Register and Alter share one validator — username length, email shape, required names, and a strict YYYY-MM-DD date-of-birth parse that rejects impossible calendar dates by round-tripping the parsed date. Both map uniqueness failures to the same friendly duplicate messages, so raw database errors never reach the client.</p>
+
+          <h3>The user store</h3>
+          <p><code>UserService</code> is injected per request, every method <code>async</code> and <code>throws Error</code>. Its authenticate path returns <code>null</code> for an unknown username, a wrong password or a disabled account — the same generic failure each time, with a dummy hash check burned for unknown usernames so both paths take comparable time. Permission grants are idempotent (no duplicate row when already held); revocation is a single targeted delete of the matching rows; and stale ids raise <code>UserServiceError.USER_NOT_FOUND</code> instead of crashing.</p>
+
+          <h3>The spry CLI</h3>
+          <figure class="code-plate">
+            <pre><code>spry new my-app      # scaffold an application, packaged for USM
+spry add page        # grow it inside marker blocks
+spry add action
+spry add resource
+spry add login       # auth UI bound to Spry.Actions
+spry add register
+spry add user-management
+spry keys            # maintain the static key pairs
+spry dev             # build, run, watch — a failed build keeps the old process
+spry deploy          # build the USM container image</code></pre>
+            <figcaption>The command surface, from Spry's README — <code>spry --help</code> lists it in full.</figcaption>
+          </figure>
+          <p><code>spry new</code> includes the USM packaging — <code>MANIFEST.usm</code>, <code>usm-scripts/</code>, <code>.usmignore</code> — so a generated application is installable from birth. <code>spry deploy</code> delegates to <code>usm manifest deploy</code>: the application's manifest drives an in-container install where the system package manager resolves the platform libraries and USM repositories resolve the Web-Stack, and the image is saved as <code>&lt;app&gt;-&lt;version&gt;.image.tar.xz</code> for <code>podman load -i</code>. Target systems include Fedora, Debian, Ubuntu, Alpine and Gentoo. Keys are never packaged — <code>.usmignore</code> excludes <code>web-config.json</code> — so an image serves plain pages with ephemeral keys, or full authentication when the config is bind-mounted read-only.</p>
+
+          <header class="section-head" id="package">
+            <p class="kicker"><span>Package &amp; source</span></p>
+            <h2>The ledger</h2>
+          </header>
+          <dl class="docs-package">
+            <div><dt>Version</dt><dd><code>0.2</code> (from <code>MANIFEST.usm</code>)</dd></div>
+            <div><dt>Licence</dt><dd>LGPL-3.0-or-later — free to run, study, share and improve; the LGPL keeps it free to link from any program.</dd></div>
+            <div><dt>Package</dt><dd><code>usm install spry</code></dd></div>
+            <div><dt>Provides</dt><dd><code>libspry-0.2.so</code>, <code>spry</code> (the CLI), <code>spry-0.2.h</code>, <code>spry-0.2.vapi</code> + <code>.deps</code>, <code>spry-0.2.gir</code>, <code>spry-0.2.pc</code></dd></div>
+            <div><dt>Needs at runtime</dt><dd>GLib and GObject, json-glib, libsodium, plus the stack beneath: <code>libinvercargill-1.so</code>, <code>libinversion-0.1.so</code>, <code>libastralis-0.1.so</code>, <code>libstatum-0.1.so</code>, <code>libinvercargill-sql.so</code> and <code>libinvercargill-sql-inversion.so</code>.</dd></div>
+            <div><dt>Source</dt><dd>The repository <code>Tilo15/Spry</code> on the Astrologue forge (git over ssh, port 222). Free software: the licence guarantees your right to the source.</dd></div>
+          </dl>
+          <p>One honest gap: the package ships a GIR but no typelib. Spry's migration classes derive from a class in a nested namespace of Invercargill-Sql, and the Vala 0.56 GIR writer drops the intermediate namespace segment when naming a parent from another library — so <code>g-ir-compiler</code> cannot resolve the parent type and the typelib cannot be built. The GIR is installed, and the packaging records the blocker; Vala and C consumers are unaffected.</p>
+
+          <nav class="docs-siblings" aria-label="Neighbours in the catalogue">
+            <p>Its neighbours:</p>
+            <ul>
+              <li><a href="../statum/">Statum</a> — the framework it extends</li>
+              <li><a href="../invercargill-sql/">Invercargill-Sql</a> and <a href="../invercargill-sql-inversion/">Invercargill-Sql-Inversion</a> — its schema and wiring</li>
+              <li><a href="../locksmith/">Locksmith</a> — the stack's cryptography, next in the catalogue</li>
+            </ul>
+          </nav>
+
+          <nav class="docs-pager" aria-label="Project pages">
+            <a class="docs-pager__link docs-pager__link--prev" href="../statum/">
+              <span>Previous</span>
+              <strong>Statum</strong>
+            </a>
+            <p class="docs-pager__up"><a href="../">All eleven</a></p>
+            <a class="docs-pager__link docs-pager__link--next" href="../locksmith/">
+              <span>Next in the catalogue</span>
+              <strong>Locksmith</strong>
+            </a>
+          </nav>
+
+        </div>
+      </div>
+    </section>
+
+  </main>
+
+  <footer class="site-footer">
+    <div class="container">
+      <div class="footer-grid">
+        <div class="foot-brand">
+          <img class="brand__logo brand__logo--dark" src="../../assets/img/astrologue-dark.png" width="392" height="89" alt="Astrologue">
+          <p>An operating system for change — kept for self sufficiency, resilience, and software freedom, so regular people can hold the power of general purpose computers in their own hands.</p>
+        </div>
+        <nav aria-label="The four freedoms">
+          <p class="foot-title">The Four Freedoms</p>
+          <ol class="freedoms">
+            <li><strong>Run</strong> the software as you wish, for any purpose.</li>
+            <li><strong>Study</strong> the source and change it to do your computing as you wish.</li>
+            <li><strong>Share</strong> copies so you can help others.</li>
+            <li><strong>Modify</strong> and publish your versions so everyone benefits.</li>
+          </ol>
+        </nav>
+        <nav aria-label="Site">
+          <p class="foot-title">Site</p>
+          <ul class="foot-list">
+            <li><a href="../../">Home</a></li>
+            <li><a href="../../#news">News</a></li>
+            <li><a href="../../#download">Download</a></li>
+            <li><a href="../">Developer</a></li>
+          </ul>
+        </nav>
+      </div>
+      <p class="colophon">Set in Cinzel and EB Garamond (SIL Open Font License). Astrologue is free software, licensed GPL-3.0-or-later: take it, tend it, pass it on. The libraries are LGPL-3.0-or-later, so they remain free to link.</p>
+    </div>
+  </footer>
+</body>
+</html>

+ 322 - 0
developer/statum/index.html

@@ -0,0 +1,322 @@
+<!doctype html>
+<html lang="en-nz">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Statum — Astrologue Developer</title>
+  <meta name="description" content="Statum is the page rendering and state management layer for Astrologue web applications — server-rendered pages, signed and encrypted client-carried state slots, GUID actions and realtime channels, bound by a small client script. Free software, LGPL-3.0-or-later.">
+  <meta name="color-scheme" content="light dark">
+  <meta name="theme-color" content="#f2ebda" media="(prefers-color-scheme: light)">
+  <meta name="theme-color" content="#0a0a12" media="(prefers-color-scheme: dark)">
+  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='27' fill='none' stroke='%23a8801f' stroke-width='4'/%3E%3Cpath d='M32 8l6 18 18 6-18 6-6 18-6-18-18-6 18-6z' fill='%23a8801f'/%3E%3Ccircle cx='32' cy='32' r='4' fill='%23003078'/%3E%3C/svg%3E">
+  <meta property="og:title" content="Statum — Astrologue Developer">
+  <meta property="og:description" content="Page rendering and state management for web applications: server-rendered pages, signed and encrypted state slots, directives and actions, bound by a small client script.">
+  <meta property="og:type" content="website">
+  <link rel="stylesheet" href="../../assets/css/style.css">
+</head>
+<body>
+  <a class="skip-link" href="#main">Skip to content</a>
+
+  <header class="masthead">
+    <div class="container masthead__inner">
+      <a class="brand" href="../../">
+        <img class="brand__logo brand__logo--light" src="../../assets/img/astrologue-light.png" width="392" height="89" alt="Astrologue">
+        <img class="brand__logo brand__logo--dark" src="../../assets/img/astrologue-dark.png" width="392" height="89" alt="Astrologue">
+      </a>
+      <nav class="site-nav" aria-label="Primary">
+        <ul class="site-nav__list">
+          <li><a href="../../">Home</a></li>
+          <li><a href="../../#news">News</a></li>
+          <li><a href="../../#download">Download</a></li>
+          <li><a href="../" aria-current="page">Developer</a></li>
+          <li><a class="nav-cta" href="../../#download">Get Astrologue</a></li>
+        </ul>
+      </nav>
+    </div>
+  </header>
+
+  <main id="main">
+
+    <nav class="breadcrumb" aria-label="Breadcrumb">
+      <div class="container">
+        <ol>
+          <li><a href="../../">Astrologue</a></li>
+          <li><a href="../">Developer</a></li>
+          <li aria-current="page">Statum</li>
+        </ol>
+      </div>
+    </nav>
+
+    <section class="docs-header">
+      <div class="container">
+        <p class="kicker"><span>Web stack · Pages and state</span></p>
+        <h1>Statum</h1>
+        <p class="lede">Page rendering and state management for web applications. HTML pages are composed and precompressed at build time; application state is signed, sealed in the browser and carried back with each request — so the server stays stateless between requests, with no session store and no database for ephemeral state. A small client script binds it all to the DOM.</p>
+        <dl class="docs-facts">
+          <div><dt>Version</dt> <dd><code>0.1</code></dd></div>
+          <div><dt>Licence</dt> <dd>LGPL-3.0-or-later</dd></div>
+          <div><dt>Package</dt> <dd><code>usm&nbsp;install&nbsp;statum</code></dd></div>
+          <div><dt>Language</dt> <dd>Vala</dd></div>
+        </dl>
+      </div>
+    </section>
+
+    <section class="band band--plain" id="docs">
+      <div class="container docs-layout">
+
+        <nav class="docs-toc" aria-label="On this page">
+          <h2>On this page</h2>
+          <ul>
+            <li><a href="#overview">Overview</a></li>
+            <li><a href="#features">Features</a></li>
+            <li><a href="#installation">Installation</a></li>
+            <li><a href="#usage">Usage &amp; documentation</a></li>
+            <li><a href="#package">Package &amp; source</a></li>
+          </ul>
+        </nav>
+
+        <div class="docs-prose">
+
+          <header class="section-head" id="overview">
+            <p class="kicker"><span>Overview</span></p>
+            <h2>The request, drawn out</h2>
+          </header>
+          <p>Statum sits on <a href="../astralis/">Astralis</a> and turns an HTTP server into an application framework. A request follows one shape, from first byte to bound DOM:</p>
+          <figure class="code-plate">
+            <pre><code> Browser                          Server
+ ───────                          ──────
+ GET / ──────────────────────►   StatumPage (precompressed HTML)
+ page loads, statum.js boots
+ GET /_statum/entrypoint ────►   StatumEntrypoint → directives
+ client applies directives        (set/clear/…)
+ client binds stm-* attributes
+
+ POST /_statum/action/{guid} ►  StatumAction → directives
+ (with X-Statum-Slot headers)    (mutate state, push to channels)</code></pre>
+            <figcaption>The architecture at a glance, from <code>getting-started.md</code>.</figcaption>
+          </figure>
+          <p>The trick is where state lives. The server mints <em>slots</em> — units of application state, identified by a type name and a random key — signs them, and hands them to the browser. The client holds them and sends them back on every subsequent request. There is no session table to scale, no sticky routing to arrange; a restarted server with the same static keys verifies and continues where it left off.</p>
+          <p>A page's route is declared in its own HTML, its handler pairs with it by type, and the wiring is three lines:</p>
+          <figure class="code-plate">
+            <pre><code>void main(string[] args) {
+    var app = new WebApplication(8080);
+    app.add_module&lt;StatumModule&gt;();
+
+    var statum = app.configure_with&lt;StatumConfigurator&gt;();
+    statum.add_page&lt;HomePage, HomeEntrypoint&gt;();   // route from &lt;pstm-uri&gt;
+    statum.action&lt;BumpAction&gt;();                   // GUID auto-assigned
+    app.run();
+}</code></pre>
+            <figcaption>From <code>getting-started.md</code> and <code>example/Main.vala</code>.</figcaption>
+          </figure>
+
+          <header class="section-head" id="features">
+            <p class="kicker"><span>Features</span></p>
+            <h2>What it hands you</h2>
+          </header>
+          <ul class="docs-features">
+            <li><strong>Server-rendered pages.</strong> HTML composed from templates and fragments at build time by <code>statum-mkpstm</code>, precompressed with gzip, zstd and brotli and served with encoding negotiation and ETags.</li>
+            <li><strong>Client-carried state.</strong> Signed snapshots — and for private data, encrypted ones — that the browser holds and returns, keeping the server stateless between requests.</li>
+            <li><strong>Entrypoint hydration.</strong> After a page loads, the client fetches directives that set the initial slot data and bind the DOM.</li>
+            <li><strong>GUID actions.</strong> POST and GET handlers auto-mapped to <code>/_statum/action/{guid}</code>, invoked from <code>stm-action</code> attributes, carrying strongly-typed private data.</li>
+            <li><strong>Realtime updates.</strong> A SharedWorker-backed SSE channel pushes slot changes to every subscribed tab.</li>
+            <li><strong>A topic system.</strong> External services — workers, webhooks, timers — trigger updates that load an entity and derive slot state for watching clients.</li>
+            <li><strong>Three build tools.</strong> <code>statum-mkpstm</code> for pages, <code>statum-mkres</code> for precompressed resources, <code>statum-genkeys</code> for static signing and encryption keys.</li>
+          </ul>
+
+          <header class="section-head" id="installation">
+            <p class="kicker"><span>Installation</span></p>
+            <h2>Getting the library</h2>
+          </header>
+          <p>With USM on your machine, one command acquires, builds and installs the library, its three command-line tools, C header, Vala binding and introspection:</p>
+          <figure class="code-plate">
+            <pre><code>usm install statum</code></pre>
+            <figcaption>Installs from the Astrologue package repository.</figcaption>
+          </figure>
+          <p>From source, the build lives at the repository root:</p>
+          <figure class="code-plate">
+            <pre><code>git clone &lt;the Statum repository&gt;
+cd Statum
+meson setup . build
+ninja -C build
+ninja -C build install</code></pre>
+            <figcaption>The same meson invocation the package's own build script uses.</figcaption>
+          </figure>
+          <p>Then compile against it with Vala's usual package mechanism:</p>
+          <figure class="code-plate">
+            <pre><code>valac --pkg statum-0.1 my-app.vala</code></pre>
+            <figcaption>The installed <code>statum-0.1.deps</code> resolves the Astralis, Invercargill and Inversion bindings for you.</figcaption>
+          </figure>
+
+          <header class="section-head" id="usage">
+            <p class="kicker"><span>Usage &amp; documentation</span></p>
+            <h2>Pages, slots and actions</h2>
+          </header>
+          <p>Statum's documentation lives in its repository: <code>getting-started.md</code> is a full walkthrough of templates, slots, actions, the client script and the topic system, and <code>model.md</code> records the design. What follows is that guide in miniature.</p>
+
+          <h3>A page, a template, an entrypoint</h3>
+          <p>A page is an HTML file whose <code>&lt;head&gt;</code> declares its own route and the layout it wraps in — all <code>pstm-*</code> directives are build-time, processed by <code>statum-mkpstm</code> into a Vala class:</p>
+          <figure class="code-plate">
+            <pre><code>&lt;!-- home.html --&gt;
+&lt;pstm-uri&gt;/&lt;/pstm-uri&gt;
+&lt;pstm-template&gt;main&lt;/pstm-template&gt;
+
+&lt;p&gt;Counter: &lt;span stm-text="counter.value"&gt;0&lt;/span&gt;&lt;/p&gt;
+&lt;button stm-action="counter.bump"&gt;Bump&lt;/button&gt;</code></pre>
+            <figcaption>From <code>getting-started.md</code> — the route comes from <code>&lt;pstm-uri&gt;</code>, never passed by hand.</figcaption>
+          </figure>
+          <p>Templates name themselves with <code>&lt;pstm-name&gt;</code> and receive child content at <code>&lt;pstm-content&gt;</code>; fragments are inlined by name with <code>&lt;pstm-fragment&gt;</code>; and when a page is composed into its template the child's head tags merge into the parent's, so every layer can declare its own styles and scripts.</p>
+          <p>After the page loads, an entrypoint hydrates it — a handler that returns directives:</p>
+          <figure class="code-plate">
+            <pre><code>public class HomeEntrypoint : StatumEntrypoint {
+    public override async DirectiveBuilder handle() throws GLib.Error {
+        var counter = new CounterPublic();
+        counter.value = 0;
+        counter.bump = action_registry
+            .author_private&lt;BumpAction, BumpPrivate&gt;(new BumpPrivate());
+        return directives().set_typed&lt;CounterPublic&gt;("counter", Scope.PAGE, counter);
+    }
+}</code></pre>
+            <figcaption>From <code>getting-started.md</code> — the counter slot is page-scoped; the bump action reference carries sealed private data.</figcaption>
+          </figure>
+
+          <h3>Slots and scopes</h3>
+          <p>A slot's data is a snapshot — public plus private properties — wrapped in a frame the server signs. The client holds frames and returns them; private data is encrypted under a namespace (X25519 seal plus Ed25519 signature), so an action's private blob cannot be read by another action, and a stolen copy of public data cannot impersonate anyone. Four scopes decide how long a slot lives:</p>
+          <ul>
+            <li><strong><code>Scope.PAGE</code></strong> — in-memory, gone on navigate; transient page data.</li>
+            <li><strong><code>Scope.FLOW</code></strong> — per tab via sessionStorage; a multi-step flow.</li>
+            <li><strong><code>Scope.SESSION</code></strong> — per browser via localStorage; login state, preferences.</li>
+            <li><strong><code>Scope.DEVICE</code></strong> — per device, long-lived settings.</li>
+          </ul>
+
+          <h3>Actions</h3>
+          <p>Actions are not addressed by URL. The server authors a reference, embeds it in slot public data, and the client invokes it through an <code>stm-action</code> attribute. A typed action decrypts its private data before <code>handle()</code> runs:</p>
+          <figure class="code-plate">
+            <pre><code>public class BumpAction : TypedStatumAction&lt;BumpPrivate&gt; {
+    public override async DirectiveBuilder handle() throws GLib.Error {
+        var delta = request_private != null ? ((!)request_private).delta : 1;
+        return directives()
+            .update_held&lt;CounterPublic&gt;("counter", c =&gt; c.value += delta);
+    }
+}</code></pre>
+            <figcaption>From <code>getting-started.md</code> — <code>update_held</code> reads, mutates and re-signs the held slot, preserving action references.</figcaption>
+          </figure>
+          <p>Directives are a small, fluent vocabulary: <code>set</code> and <code>set_typed</code> sign state into the client, <code>update_held</code> mutates what is already held, <code>clear</code> drops it, <code>navigate</code> redirects, <code>notify</code> surfaces a message, <code>subscribe</code> joins a realtime channel — and terminal directives like <code>form_submit</code> finish a round trip by submitting a form by id.</p>
+
+          <h3>The client script</h3>
+          <p><code>statum.js</code> is embedded in the library and served from <code>/_statum/resource/statum.js</code> — one script tag in the template, auto-initialising on DOMContentLoaded. Binding attributes do the rest:</p>
+          <figure class="code-plate">
+            <pre><code>&lt;p&gt;Counter: &lt;span stm-text="counter.value"&gt;0&lt;/span&gt;&lt;/p&gt;
+&lt;button stm-action="counter.bump"&gt;Bump&lt;/button&gt;
+
+&lt;div stm-if="auth != null"&gt; … &lt;/div&gt;
+&lt;div stm-else&gt; … &lt;/div&gt;
+
+&lt;div stm-class.active="cart.count &gt; 0"&gt; … &lt;/div&gt;
+&lt;div stm-markdown="post.body"&gt;&lt;/div&gt;</code></pre>
+            <figcaption>From <code>getting-started.md</code> — text, HTML, CommonMark markdown, conditionals, class toggles and action calls.</figcaption>
+          </figure>
+          <p>Its markdown renderer targets the CommonMark 0.31.2 specification — all source HTML escaped first — and its JavaScript test suite runs under a vendored QuickJS, reporting conformance against the official example suite and failing below a 95% target.</p>
+
+          <h3>Topics and global slots</h3>
+          <p>A topic loads an entity from the database when triggered; a state modifier derives slot types from the payload and pushes them to subscribed clients over SSE. The example registers a topic that cycles an announcement every ten seconds, so two tabs side by side show the broadcast arriving in both:</p>
+          <figure class="code-plate">
+            <pre><code>statum.global&lt;AnnouncementPublic&gt;("announcement", announcement);
+statum.topic&lt;AnnouncementTopic&gt;("announcement");
+statum.topic_modifier&lt;AnnouncementPreviewModifier, AnnouncementPreview&gt;("announcement");
+
+// trigger from anywhere — a worker, a webhook, a timer:
+yield topic_registry.trigger("announcement");</code></pre>
+            <figcaption>From <code>example/Main.vala</code> and <code>getting-started.md</code> — global slots use deterministic keys for broadcast.</figcaption>
+          </figure>
+
+          <h3>Keys and models</h3>
+          <p>Signed frames and encrypted blobs need keys that outlive a restart. <code>statum-genkeys</code> prints a ready-to-paste <code>statum</code> section for <code>web-config.json</code>; without it the server generates random keys per restart, which is fine for development and logged with a warning.</p>
+          <p>Typed models serialise by GObject property introspection — scalars including <code>int64</code> and <code>bool</code> round-trip as type-correct JSON primitives on both the public and the encrypted path, <code>string[]</code> properties come back as arrays, and Invercargill collections serialise as JSON arrays. The test suite pins exactly this:</p>
+          <figure class="code-plate">
+            <pre><code>var model = new LoginPrivate() {
+    user_id = (int64)9007199254740993,
+    is_admin = true,
+    attempts = 3,
+    score = 0.5,
+    username = "bbarrow",
+    session_no = (long)123456789012345
+};
+
+var wire = new JsonElement.from_properties(
+    GObjectMapping.to_properties(model)).stringify();
+check(wire.contains("\"user_id\":9007199254740993"),
+    "public path: int64 serialises as an unquoted JSON number");</code></pre>
+            <figcaption>From <code>tests/TestMain.vala</code> — the suite runs through <code>meson test</code> and USM's test exec.</figcaption>
+          </figure>
+
+          <header class="section-head" id="package">
+            <p class="kicker"><span>Package &amp; source</span></p>
+            <h2>The ledger</h2>
+          </header>
+          <dl class="docs-package">
+            <div><dt>Version</dt><dd><code>0.1</code> (from <code>MANIFEST.usm</code>)</dd></div>
+            <div><dt>Licence</dt><dd>LGPL-3.0-or-later — free to run, study, share and improve; the LGPL keeps it free to link from any program.</dd></div>
+            <div><dt>Package</dt><dd><code>usm install statum</code></dd></div>
+            <div><dt>Provides</dt><dd><code>libstatum-0.1.so</code>, <code>statum-mkpstm</code>, <code>statum-mkres</code>, <code>statum-genkeys</code>, <code>statum-0.1.h</code>, <code>statum-0.1.vapi</code> + <code>.deps</code>, <code>statum-0.1.gir</code>, <code>statum-0.1.typelib</code>, <code>statum-0.1.pc</code></dd></div>
+            <div><dt>Needs at runtime</dt><dd>GLib and GObject, libsodium, libxml2, json-glib via the stack beneath — <code>libinvercargill-1.so</code>, <code>libinvercargill-json.so</code>, <code>libinversion-0.1.so</code> and <code>libastralis-0.1.so</code>.</dd></div>
+            <div><dt>Source</dt><dd>The repository <code>Tilo15/Statum</code> on the Astrologue forge (git over ssh, port 222). Free software: the licence guarantees your right to the source.</dd></div>
+          </dl>
+
+          <nav class="docs-siblings" aria-label="Neighbours in the catalogue">
+            <p>Its neighbours:</p>
+            <ul>
+              <li><a href="../astralis/">Astralis</a> — the server beneath it</li>
+              <li><a href="../spry/">Spry</a> — authentication and user management on top of it</li>
+            </ul>
+          </nav>
+
+          <nav class="docs-pager" aria-label="Project pages">
+            <a class="docs-pager__link docs-pager__link--prev" href="../astralis/">
+              <span>Previous</span>
+              <strong>Astralis</strong>
+            </a>
+            <p class="docs-pager__up"><a href="../">All eleven</a></p>
+            <a class="docs-pager__link docs-pager__link--next" href="../spry/">
+              <span>Next in the catalogue</span>
+              <strong>Spry</strong>
+            </a>
+          </nav>
+
+        </div>
+      </div>
+    </section>
+
+  </main>
+
+  <footer class="site-footer">
+    <div class="container">
+      <div class="footer-grid">
+        <div class="foot-brand">
+          <img class="brand__logo brand__logo--dark" src="../../assets/img/astrologue-dark.png" width="392" height="89" alt="Astrologue">
+          <p>An operating system for change — kept for self sufficiency, resilience, and software freedom, so regular people can hold the power of general purpose computers in their own hands.</p>
+        </div>
+        <nav aria-label="The four freedoms">
+          <p class="foot-title">The Four Freedoms</p>
+          <ol class="freedoms">
+            <li><strong>Run</strong> the software as you wish, for any purpose.</li>
+            <li><strong>Study</strong> the source and change it to do your computing as you wish.</li>
+            <li><strong>Share</strong> copies so you can help others.</li>
+            <li><strong>Modify</strong> and publish your versions so everyone benefits.</li>
+          </ol>
+        </nav>
+        <nav aria-label="Site">
+          <p class="foot-title">Site</p>
+          <ul class="foot-list">
+            <li><a href="../../">Home</a></li>
+            <li><a href="../../#news">News</a></li>
+            <li><a href="../../#download">Download</a></li>
+            <li><a href="../">Developer</a></li>
+          </ul>
+        </nav>
+      </div>
+      <p class="colophon">Set in Cinzel and EB Garamond (SIL Open Font License). Astrologue is free software, licensed GPL-3.0-or-later: take it, tend it, pass it on. The libraries are LGPL-3.0-or-later, so they remain free to link.</p>
+    </div>
+  </footer>
+</body>
+</html>