Selaa lähdekoodia

Add Locksmith, Vallama and USM project pages

Three project detail pages on the docs template: security (Locksmith), intelligence (Vallama) and packaging (USM). All copy mined from the repos' own READMEs, MANIFEST.usm and examples - versions from the manifests, licences verified against the LICENSE files (LGPL-3.0-or-later, LGPL-3.0-or-later, GPL-3.0-or-later), provides and runtime deps from the merged goal/website manifests, source repositories from the actual origin URLs.

Pager order follows the catalogue: spry -> locksmith -> vallama -> usm -> back to the developer index. The three pages come off check.py's UPCOMING_PAGES allowlist (A4's astralis/statum/spry stay allowlisted). tools/qa-pages.py is the CDP harness used to verify light/dark rendering and zero horizontal overflow at 390/768/1280.
saddle-ClankerSaddle 2 viikkoa sitten
vanhempi
sitoutus
2feca448c0
5 muutettua tiedostoa jossa 970 lisäystä ja 6 poistoa
  1. 4 6
      check.py
  2. 262 0
      developer/locksmith/index.html
  3. 251 0
      developer/usm/index.html
  4. 269 0
      developer/vallama/index.html
  5. 184 0
      tools/qa-pages.py

+ 4 - 6
check.py

@@ -26,16 +26,14 @@ 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.
+# TODO(A4): the three remaining detail pages land in wave 3; strip these
+# entries as each wave merges. The five Invercargill-family pages (A3) and
+# the security/intelligence/packaging trio (A5: locksmith, vallama, usm)
+# have landed and are enforced.
 UPCOMING_PAGES = {
     "developer/astralis/",
     "developer/statum/",
     "developer/spry/",
-    "developer/locksmith/",
-    "developer/vallama/",
-    "developer/usm/",
 }
 
 

+ 262 - 0
developer/locksmith/index.html

@@ -0,0 +1,262 @@
+<!doctype html>
+<html lang="en-nz">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Locksmith — Astrologue Developer</title>
+  <meta name="description" content="Locksmith is developer-friendly cryptography for Vala: sodium boxes, X.509, SSH, OpenPGP and WebPush over libsodium, GnuTLS, GPGME and libssh — it implements no cryptography itself. 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="Locksmith — Astrologue Developer">
+  <meta property="og:description" content="Developer-friendly cryptography in objects, not byte arrays: sodium boxes, X.509, SSH, OpenPGP and WebPush over libsodium, GnuTLS, GPGME and libssh.">
+  <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">Locksmith</li>
+        </ol>
+      </div>
+    </nav>
+
+    <section class="docs-header">
+      <div class="container">
+        <p class="kicker"><span>Security · Keys, seals and signatures</span></p>
+        <h1>Locksmith</h1>
+        <p class="lede">Developer-friendly cryptography for Vala, in objects rather than byte arrays. It wraps the battle-tested libraries you already have — libsodium, GnuTLS, GPGME and libssh — and implements no cryptography itself.</p>
+        <dl class="docs-facts">
+          <div><dt>Version</dt> <dd><code>0.1.0</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;locksmith</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>Why it exists</h2>
+          </header>
+          <p>Locksmith is an all-purpose cryptography library for Vala. It wraps the crypto libraries a machine already carries — libsodium, GnuTLS, GPGME and libssh — behind simple, object-oriented APIs, so an application gets secure cryptography without touching a byte array unless it wants to.</p>
+          <p>The pledge is the design: <strong>Locksmith implements no cryptography itself.</strong> Every primitive comes from those four libraries; Locksmith contributes the object model, the wire formats and the defaults. Nothing new can be wrong in a way its underlying libraries are not, and an audit of the cryptography is an audit of software that has already been audited.</p>
+          <p>Its wire formats are the ones the rest of the Astrologue stack already uses: Ed25519 attached signatures are <code>signature||message</code> (USM's format), seals use the X25519 ephemeral-header form, and keys are their raw bytes — so <a href="../usm/">USM</a> and <a href="../statum/">Statum</a> signed slots can move onto Locksmith without changing a byte on disk. A compatibility test suite proves both directions by compiling the original hand-rolled bindings alongside this library.</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>Signing.</strong> Ed25519 keypairs and public keys; attached (<code>signature||message</code>, the USM wire format) and detached signatures.</li>
+            <li><strong>Sealing.</strong> X25519 anonymous seals and authenticated two-party boxes.</li>
+            <li><strong>Symmetric.</strong> Secretbox keys and ciphers; the nonce travels inside the ciphertext, so it cannot be forgotten.</li>
+            <li><strong>Hashing.</strong> One-call and streaming digests (SHA-1/2/3, SHAKE, MD5, BLAKE2b), HMAC, keyed BLAKE2b, PBKDF2, and argon2id password hashing — the only API pointed at passwords.</li>
+            <li><strong>Certificates.</strong> X.509 via GnuTLS: RSA/ECDSA/Ed25519 keypairs, DistinguishedNames, certificates, PKCS#10 requests, and Authority objects that issue and verify.</li>
+            <li><strong>Ssh.</strong> OpenSSH keys via libssh: generate, load <code>id_ed25519</code> files and <code>authorized_keys</code> lines, fingerprints, passphrase-protected saves, and SSHSIG sign/verify that interops with <code>ssh-keygen&nbsp;-Y</code>.</li>
+            <li><strong>Pgp.</strong> OpenPGP via GPGME: hermetic keyrings, key generation, sign/verify, encrypt/decrypt, import/export, armored output.</li>
+            <li><strong>WebPush.</strong> RFC 8291 message encryption and RFC 8292 VAPID headers via GnuTLS: browser subscription JSON in, POST-ready body and headers out.</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 with its header, Vala binding and introspection:</p>
+          <figure class="code-plate">
+            <pre><code>usm install locksmith</code></pre>
+            <figcaption>USM resolves <code>invercargill-1</code> and <code>invercargill-json</code> itself.</figcaption>
+          </figure>
+          <p>Building by hand needs the four crypto libraries plus the stack's foundation — on Fedora:</p>
+          <figure class="code-plate">
+            <pre><code>sudo dnf install meson ninja-build vala libsodium-devel gnutls-devel \
+    gpgme-devel libssh-devel libgpg-error-devel json-glib-devel \
+    invercargill-devel
+
+git clone &lt;the Locksmith repository&gt;
+cd Locksmith
+meson setup build &amp;&amp; ninja -C build &amp;&amp; meson test -C build</code></pre>
+            <figcaption>The Fedora line and the build commands are the README's own.</figcaption>
+          </figure>
+          <p>Then compile against it with Vala's usual package mechanism:</p>
+          <figure class="code-plate">
+            <pre><code>valac --pkg locksmith-1 my-program.vala</code></pre>
+            <figcaption>The package ships its five internal bindings alongside, so <code>--pkg locksmith-1</code> resolves everything.</figcaption>
+          </figure>
+
+          <header class="section-head" id="usage">
+            <p class="kicker"><span>Usage &amp; documentation</span></p>
+            <h2>Quick start, from the README</h2>
+          </header>
+          <p>Signing, the way USM signs a package listing — but with objects:</p>
+          <figure class="code-plate">
+            <pre><code>var pair = new Locksmith.Signing.KeyPair.generate();
+var signed = pair.sign(new ByteBuffer.from_byte_array(listing.data));
+var recovered = pair.public_key.verify(signed);   // null when invalid</code></pre>
+            <figcaption>Verbatim from the README's Quickstart. Verification returns null rather than throwing — invalid input is a normal outcome.</figcaption>
+          </figure>
+          <p>Sealing (anonymous: only the recipient can open) and symmetric encryption (the nonce travels inside):</p>
+          <figure class="code-plate">
+            <pre><code>var recipient = new Locksmith.Sealing.KeyPair.generate();
+var sealed = recipient.public_key.seal(message);
+var opened = recipient.unseal(sealed);
+
+var key = new Locksmith.Symmetric.Key.generate();
+var cipher = new Locksmith.Symmetric.Cipher();
+var stored = cipher.encrypt(key, secret);
+var back = cipher.decrypt(key, stored);   // throws CIPHER when tampered</code></pre>
+            <figcaption>From the README. Tamper detection is proven by the test suite, not just round-trips.</figcaption>
+          </figure>
+          <p>A password is never hashed with a digest — it goes to argon2id, and verification is a method on the same object:</p>
+          <figure class="code-plate">
+            <pre><code>var phc = new Locksmith.Hashing.PasswordHash().hash(password);
+// verify later: hasher.verify(phc, attempt)</code></pre>
+            <figcaption>Digested data is labelled integrity-only, never password-safe.</figcaption>
+          </figure>
+          <h3>Certificates and SSH keys</h3>
+          <p>An Authority issues and verifies leaves; an SSH key saves and signs in OpenSSH's own formats:</p>
+          <figure class="code-plate">
+            <pre><code>var root = Locksmith.Certificates.Authority.generate(
+    Locksmith.Certificates.KeyType.ED25519, subject, 3650);
+var leaf = root.issue_for_public_key(leaf_subject, server.public_key,
+    365, sans);
+string reason;
+leaf.verify(root.certificate, out reason);
+
+var key = new Locksmith.Ssh.Key.generate(Locksmith.Ssh.KeyType.ED25519,
+    "me@host");
+print("%s\n", key.public_string);   // authorized_keys line
+var signature = key.sign(file_contents);   // SSHSIG</code></pre>
+            <figcaption>From the README's Quickstart — the SSHSIG output verifies with <code>ssh-keygen -Y</code>.</figcaption>
+          </figure>
+          <h3>OpenPGP and WebPush</h3>
+          <figure class="code-plate">
+            <pre><code>var ctx = new Locksmith.Pgp.Context.create();
+ctx.home_directory = keyring_dir;
+var key = ctx.generate_key("Alice &lt;alice@example.org&gt;");
+var signed = ctx.sign(document);
+
+var subscription = new Locksmith.WebPush.Subscription.from_json(json);
+var vapid = new Locksmith.WebPush.ApplicationServerKeys.generate();
+var message = Locksmith.WebPush.encrypt(subscription, payload);
+var headers = Locksmith.WebPush.Vapid.headers(vapid,
+    "https://push.example.net", "mailto:admin@example.org");</code></pre>
+            <figcaption>A hermetic GPGME keyring for the first; the application-server half of RFC 8291 + 8292 for the second — POST-ready output.</figcaption>
+          </figure>
+          <h3>The API philosophy</h3>
+          <p>Every key, nonce, signature, digest and ciphertext is an <code>Invercargill.BinaryData</code> (concretely <code>ByteBuffer</code>), so <code>to_base64()</code>, <code>to_hex()</code> and equality come free. What a constructor accepts, a method gives back: <code>from_base64</code>/<code>from_hex</code>/<code>from_bytes</code> against <code>to_base64</code>/<code>to_hex</code>/<code>to_der</code>/<code>to_pem</code>. Malformed input throws a <code>Locksmith.Error</code> code; verification — where invalid input is a normal outcome — returns <code>bool</code> or <code>null</code>, so control flow reads like intent.</p>
+          <p>Runnable versions of every snippet above live in <code>examples/</code> — one standalone program per subsystem (<code>signing_example.vala</code>, <code>sealing_example.vala</code>, <code>symmetric_example.vala</code>, <code>hashing_example.vala</code>, <code>certificates_example.vala</code>, <code>ssh_example.vala</code>, <code>pgp_example.vala</code>, <code>webpush_example.vala</code>), each smoke-tested by <code>meson test</code>. The library suite itself runs round-trips, tamper detection and RFC vectors through both <code>meson test</code> and USM's <code>test</code> exec, so what is documented here is what is exercised.</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.0</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. The <code>vapi/</code> libsodium binding is derived from Billy Barrow's ISC-licensed binding, credited in-file.</dd></div>
+            <div><dt>Package</dt><dd><code>usm install locksmith</code></dd></div>
+            <div><dt>Provides</dt><dd><code>liblocksmith-1.so</code>, <code>locksmith.h</code>, <code>locksmith-1.vapi</code> + <code>.deps</code> plus the five internal bindings (<code>libsodium</code>, <code>gnutls-supplement</code>, <code>gnutls-x509</code>, <code>gpgme</code>, <code>libssh</code>) in <code>share/vala/vapi</code>, <code>locksmith-1.gir</code>, <code>locksmith-1.typelib</code>, <code>locksmith-1.pc</code></dd></div>
+            <div><dt>Needs at runtime</dt><dd>GLib, GObject, GIO, <code>libinvercargill-1.so</code>, <code>libinvercargill-json.so</code>, <code>libsodium.so&nbsp;&gt;=26</code>, <code>libgnutls.so&nbsp;&gt;=30</code>, <code>libgpgme.so&nbsp;&gt;=45</code>, <code>libssh.so&nbsp;&gt;=4</code> and the <code>gpg</code> binary.</dd></div>
+            <div><dt>Source</dt><dd>The repository <code>clanker/Locksmith</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="Related in the catalogue">
+            <p>Its neighbours:</p>
+            <ul>
+              <li><a href="../statum/">Statum</a></li>
+              <li><a href="../spry/">Spry</a></li>
+              <li><a href="../usm/">USM</a></li>
+            </ul>
+          </nav>
+
+          <nav class="docs-pager" aria-label="Project pages">
+            <a class="docs-pager__link docs-pager__link--prev" href="../spry/">
+              <span>Previous</span>
+              <strong>Spry</strong>
+            </a>
+            <p class="docs-pager__up"><a href="../">All eleven</a></p>
+            <a class="docs-pager__link docs-pager__link--next" href="../vallama/">
+              <span>Next in the catalogue</span>
+              <strong>Vallama</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>

+ 251 - 0
developer/usm/index.html

@@ -0,0 +1,251 @@
+<!doctype html>
+<html lang="en-nz">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>USM — Astrologue Developer</title>
+  <meta name="description" content="USM — the Universal Source Manifest package manager: a project declares what it provides and needs in a MANIFEST.usm, and usm acquires, builds, tests, packages and deploys it. Free software, GPL-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="USM — Astrologue Developer">
+  <meta property="og:description" content="The Universal Source Manifest: a project declares what it provides and needs in a MANIFEST.usm, and usm acquires, builds, tests, packages and deploys it.">
+  <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">USM</li>
+        </ol>
+      </div>
+    </nav>
+
+    <section class="docs-header">
+      <div class="container">
+        <p class="kicker"><span>Packaging · How it all ships</span></p>
+        <h1>USM</h1>
+        <p class="lede">The Universal Source Manifest: a project declares what it provides and what it needs in one JSON file, and <code>usm</code> acquires, builds, tests, packages and deploys it. It is the package manager that ships the Astrologue stack — and any project you care to point it at.</p>
+        <dl class="docs-facts">
+          <div><dt>Version</dt> <dd><code>0.1</code></dd></div>
+          <div><dt>Licence</dt> <dd>GPL-3.0-or-later</dd></div>
+          <div><dt>Package</dt> <dd><code>usm&nbsp;install&nbsp;usm</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>Why it exists</h2>
+          </header>
+          <p>USM — the Universal Source Manifest — is a source-first package manager. A package is a directory with a <code>MANIFEST.usm</code> at its root: one JSON document naming the package, its version, its licences, every resource it provides (libraries, headers, vapis, GIRs, typelibs, pkg-config files, binaries, data) and every resource it depends on at runtime, at build time, and to manage it. From that single declaration, the <code>usm</code> command derives the whole lifecycle: acquire the sources, resolve the dependencies, build, test, install, package, publish, deploy.</p>
+          <p>Every library on this site ships as a USM source package — the <code>usm install</code> line on each page is the whole ceremony. The stack bootstraps from its own foundations: USM is written in Vala against <a href="../invercargill/">Invercargill</a> and <a href="../invercargill-json/">Invercargill-Json</a>, validates its own manifest with its own validator, and — since the packaging work of this release — runs its own test suite through its own <code>test</code> exec. The package manager packages itself.</p>
+          <p>Source-first is the point: what you install is what was built from the sources you can read, with binary-artifact repositories (DCBA) available as a cache layer rather than a chain to hold you.</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>One manifest, one truth.</strong> <code>MANIFEST.usm</code> declares provides and depends over a rich resource vocabulary — <code>lib:</code>, <code>inc:</code>, <code>vapi:</code>, <code>gir:</code>, <code>typelib:</code>, <code>pc:</code>, <code>bin:</code>, <code>res:</code>, <code>app:</code> and more — with version constraints where they matter.</li>
+            <li><strong>Signed source repositories.</strong> A repository is a directory with a descriptor, ed25519 keys and a signed <code>PACKAGES.usml</code> listing; <code>usm repository init/add/publish/verify</code> tends it, and every package is checksummed.</li>
+            <li><strong>DCBA artifact repositories.</strong> Distribute pre-built build artifacts as <code>.dcba</code> files alongside the sources — same layout, same signing, used automatically by <code>usm install</code> when a client's platform matches, with local builds always winning.</li>
+            <li><strong>Validation that builds.</strong> <code>usm manifest validate</code> builds the package, installs to a temporary directory and checks that every declared resource actually appeared — a manifest cannot claim what the build never produced.</li>
+            <li><strong>Tests in the lifecycle.</strong> A <code>test</code> exec in the manifest gives <code>usm manifest test</code> a suite to run against the built package — the whole stack's suites run this way.</li>
+            <li><strong>System package manager bridges.</strong> Missing resources can delegate to the native manager before USM repositories are tried; five reference helpers ship (<code>usm-spm-dnf</code>, <code>-apt</code>, <code>-apk</code>, <code>-emerge</code>, <code>-pacman</code>) behind one stable JSON contract.</li>
+            <li><strong>Container deployment.</strong> <code>usm manifest deploy</code> builds a container image that installs the package in-container, with the SPM bridge of your distro wired in.</li>
+          </ul>
+
+          <header class="section-head" id="installation">
+            <p class="kicker"><span>Installation</span></p>
+            <h2>Getting the manager</h2>
+          </header>
+          <p>USM installs itself like anything else it manages:</p>
+          <figure class="code-plate">
+            <pre><code>usm install usm</code></pre>
+            <figcaption>Acquires the sources, builds with meson/ninja, runs the test exec, installs.</figcaption>
+          </figure>
+          <p>From source it is the stack's one-build discipline — the manifest's build exec meson-sets-up <code>src/</code>:</p>
+          <figure class="code-plate">
+            <pre><code>git clone &lt;the usm repository&gt;
+cd usm
+meson setup src build
+ninja -C build
+ninja -C build install</code></pre>
+            <figcaption>Needs valac, meson, ninja, GLib/GObject/GIO, <code>invercargill-1</code>, <code>invercargill-json</code>, json-glib and gobject-introspection.</figcaption>
+          </figure>
+          <p>Its own libraries are usable directly, not just through the command:</p>
+          <figure class="code-plate">
+            <pre><code>valac --pkg usm-0.1 my-tool.vala</code></pre>
+            <figcaption>The manifest's <code>provides</code> lists <code>libusm-0.1.so</code>, <code>usm-0.1.h</code>, the vapi + <code>.deps</code>, GIR and typelib — USM ships as a library too.</figcaption>
+          </figure>
+
+          <header class="section-head" id="usage">
+            <p class="kicker"><span>Usage &amp; documentation</span></p>
+            <h2>The manifest, the verbs, the bridges</h2>
+          </header>
+          <p>A manifest names the package, its version, its licences, what it provides and what it depends on — this is the README's own example, trimmed to the shape:</p>
+          <figure class="code-plate">
+            <pre><code>{
+  "name": "my-package",
+  "version": "1.0.5+2",
+  "summary": "A sample package",
+  "licences": [ {"name": "GPLv3", "category": "libre", "text": "src/LICENSE"} ],
+  "provides": {
+    "bin:hello-world": "hello-world",
+    "res:usr/share/icons/hicolor/scalable/hello-world.svg": "icons/hello-world.svg"
+  },
+  "depends": {
+    "runtime": [ "lib:libgtk-4.so.1" ],
+    "build":     [ "bin:valac", "bin:meson", "inc:gtk-4.0" ],
+    "manage":    [ "bin:bash" ]
+  },
+  "execs": {
+    "build":   "usm-scripts/build",
+    "install": "usm-scripts/install",
+    "test":    "usm-scripts/test"
+  }
+}</code></pre>
+            <figcaption>From the README. Every <code>provides</code> path is checked against what the build actually installed.</figcaption>
+          </figure>
+          <h3>The verbs</h3>
+          <p>One command, grouped verbs — the descriptions below are lifted from <code>usm --help</code>:</p>
+          <figure class="code-plate">
+            <pre><code>usm install &lt;packages&gt;    Install packages (plan + confirm; -y to skip)
+usm update [packages]     Update installed packages (or all if none named)
+usm remove &lt;packages&gt;     Remove packages (cascades + sweeps orphaned deps)
+usm search &lt;query&gt;        Search repositories and installed packages
+usm provides &lt;resource&gt;   Find which package provides a resource
+usm repository add &lt;pkg&gt;  Add a package (.usmc) to this repository
+usm manifest validate     Validate a manifest
+usm manifest test         Run package tests
+usm manifest package      Create a .usmc archive
+usm manifest deploy       Deploy a package as a container image</code></pre>
+            <figcaption>A selection — <code>usm --help</code> lists the full set, including <code>dcba</code>, <code>scaffold</code> and <code>manifest bump</code>.</figcaption>
+          </figure>
+          <h3>Repositories and signatures</h3>
+          <p>A source repository is a directory with a <code>.usmr</code> descriptor, a <code>keys/</code> pair and a <code>public/</code> package directory; the listing is signed with the repository's ed25519 key and every package carries a SHA-512. Publishing is two commands:</p>
+          <figure class="code-plate">
+            <pre><code>usm repository init my-repo
+usm repository add my-package-1.0.0.usmc
+usm repository publish /srv/www/my-repo</code></pre>
+            <figcaption>On a client: <code>usm add-repo &lt;url&gt;</code>, then <code>usm install my-package</code>.</figcaption>
+          </figure>
+          <h3>The self-referential bit</h3>
+          <p>USM's own manifest declares the same execs every other package in the stack declares — <code>build</code>, <code>install</code>, <code>test</code> — so <code>usm manifest validate</code> on the usm repository builds USM with USM's build exec and <code>usm manifest test</code> runs USM's own suite (manifest parsing, <code>.usmignore</code> semantics, data-package validation, <code>usm.config</code> handling) through USM's test exec. The manager is managed.</p>
+          <p>The repository carries the full references: <code>README.md</code> for commands and repository formats, <code>SPECIFICATION.md</code> for the manifest JSON schema, and <code>PACKAGING_GUIDE.md</code> for how to package a project well — including the classic pitfall of trusting <code>ldd</code> when declaring runtime dependencies.</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>GPL-3.0-or-later — free to run, study, share and improve. A package manager is infrastructure for everyone; this one stays strongly copyleft.</dd></div>
+            <div><dt>Package</dt><dd><code>usm install usm</code></dd></div>
+            <div><dt>Provides</dt><dd><code>usm</code> the binary, <code>libusm-0.1.so</code>, <code>usm-0.1.h</code>, <code>usm-0.1.vapi</code> + <code>.deps</code>, <code>usm-0.1.gir</code>, <code>usm-0.1.typelib</code>, <code>usm-0.1.pc</code>, the five SPM bridge binaries (<code>usm-spm-dnf</code>, <code>usm-spm-apt</code>, <code>usm-spm-apk</code>, <code>usm-spm-emerge</code>, <code>usm-spm-pacman</code>), and its manifest documentation under <code>slopdocs/</code></dd></div>
+            <div><dt>Needs at runtime</dt><dd>GLib, GObject, GIO, <code>libinvercargill-1.so</code>, <code>libinvercargill-json.so</code>, json-glib, and the usual GObject platform libraries. Build time adds valac, meson, ninja and gobject-introspection.</dd></div>
+            <div><dt>Source</dt><dd>The repository <code>Tilo15/usm</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="Related in the catalogue">
+            <p>Its neighbours:</p>
+            <ul>
+              <li><a href="../invercargill/">Invercargill</a></li>
+              <li><a href="../invercargill-json/">Invercargill-Json</a></li>
+              <li><a href="../locksmith/">Locksmith</a></li>
+            </ul>
+          </nav>
+
+          <nav class="docs-pager" aria-label="Project pages">
+            <a class="docs-pager__link docs-pager__link--prev" href="../vallama/">
+              <span>Previous</span>
+              <strong>Vallama</strong>
+            </a>
+            <p class="docs-pager__up"><a href="../">All eleven</a></p>
+            <a class="docs-pager__link docs-pager__link--next" href="../">
+              <span>End of the catalogue</span>
+              <strong>All projects</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>

+ 269 - 0
developer/vallama/index.html

@@ -0,0 +1,269 @@
+<!doctype html>
+<html lang="en-nz">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Vallama — Astrologue Developer</title>
+  <meta name="description" content="Vallama is a high-level Vala library for running LLM inference with llama.cpp, with native tool calling driven by grammar-constrained decoding: chat sessions, embeddings, tokenizers and tools — on your own hardware. 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="Vallama — Astrologue Developer">
+  <meta property="og:description" content="High-level Vala bindings for llama.cpp inference: chat sessions, grammar-constrained tool calling, embeddings and tokenizers — running on your own hardware.">
+  <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">Vallama</li>
+        </ol>
+      </div>
+    </nav>
+
+    <section class="docs-header">
+      <div class="container">
+        <p class="kicker"><span>Intelligence · Language models, locally</span></p>
+        <h1>Vallama</h1>
+        <p class="lede">High-level Vala bindings for llama.cpp LLM inference: chat sessions, grammar-constrained tool calling, embeddings and tokenizers — running on your own hardware, with no cloud in sight.</p>
+        <dl class="docs-facts">
+          <div><dt>Version</dt> <dd><code>0.1.0</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;vallama</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>Why it exists</h2>
+          </header>
+          <p>Vallama is a high-level Vala library for running LLM inference with llama.cpp, with native tool calling. It wraps the model, context, sampler and tokenizer objects of the C API into a small set of Vala classes — <code>Model</code>, <code>Context</code>, <code>Sampler</code>, <code>Generator</code>, <code>ChatSession</code>, <code>Embedder</code> — and takes care of the parts of llama.cpp that bite: chat templates, grammar ordering, streaming callbacks, and the context parameters that are fixed at creation.</p>
+          <p>It is built the way the rest of the stack is built: the collections underneath are <a href="../invercargill/">Invercargill</a>, so transcripts and tool lists are <code>Enumerable</code> sequences rather than bare arrays, and the examples that ship in <code>examples/</code> are the runnable versions of every idiom on this page.</p>
+          <p>Because the model file is a local GGUF and the inference is local compute, an application built on Vallama depends on no cloud service, sends nothing anywhere, and keeps working when the network does not — the same decentralised stance the rest of Astrologue takes.</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>Native tool calling.</strong> OpenAI-style tool calling driven by grammar-constrained decoding: when tools are registered, the assistant turn is generated under a GBNF grammar compiled from the tool schemas, so the model can only emit a valid tool-call envelope.</li>
+            <li><strong>Chat sessions.</strong> <code>ChatSession</code> wraps Model + Context + Sampler, keeps the transcript, formats prompts, runs constrained turns, and offers <code>run_with_tools</code> to drive the whole loop via a callback.</li>
+            <li><strong>Streaming generation.</strong> <code>generate_stream</code> and <code>respond_stream</code> hand each decoded piece to a callback, so interfaces can print as the model thinks.</li>
+            <li><strong>KV-cache priming.</strong> <code>prime()</code> pre-evaluates a long system prompt once; <code>save()</code>/<code>load()</code> persist the resulting KV state to a file so later runs skip the expensive evaluation and start generating immediately.</li>
+            <li><strong>Embeddings.</strong> A high-level text-embedding API on llama.cpp's sequence pooling: CLS, MEAN and LAST_TOKEN pooling, L2-normalised vectors, and batched <code>embed_all</code> over independent sequences.</li>
+            <li><strong>Tokenizers.</strong> Exposed directly for piece-level work, with the grammar sampler attachable to any chain.</li>
+            <li><strong>Chat templates handled.</strong> The model's template when it expands, an exact ChatML fallback otherwise, and a hand-rolled ChatML renderer whenever tool turns are in the transcript.</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 with its headers, Vala bindings and introspection:</p>
+          <figure class="code-plate">
+            <pre><code>usm install vallama</code></pre>
+            <figcaption>USM resolves <code>invercargill-1</code> and llama.cpp itself.</figcaption>
+          </figure>
+          <p>From source, the stack's one-build discipline:</p>
+          <figure class="code-plate">
+            <pre><code>git clone &lt;the Vallama repository&gt;
+cd Vallama
+meson setup build
+ninja -C build
+ninja -C build install</code></pre>
+            <figcaption>Needs llama.cpp (with <code>llama.pc</code>), GLib and <code>invercargill-1</code> — the version this page documents is verified against llama.cpp b6153.</figcaption>
+          </figure>
+          <p>Then compile against it with Vala's usual package mechanism:</p>
+          <figure class="code-plate">
+            <pre><code>valac --pkg vallama-0.1 my-program.vala</code></pre>
+            <figcaption>The binding pulls in its <code>.deps</code> chain — GLib, GObject and Invercargill — automatically.</figcaption>
+          </figure>
+
+          <header class="section-head" id="usage">
+            <p class="kicker"><span>Usage &amp; documentation</span></p>
+            <h2>From the README and examples</h2>
+          </header>
+          <p>Generation streams piece by piece, with model, context and sampler split the way llama.cpp splits them:</p>
+          <figure class="code-plate">
+            <pre><code>Vallama.init();
+
+var model = new Model (model_path, ModelOptions.defaults ());
+var ctx_opts = ContextOptions.defaults ();
+ctx_opts.n_ctx = 2048;
+ctx_opts.n_threads = 8;
+var ctx = new Context (model, ctx_opts);
+var sampler = new Sampler (SamplerOptions.defaults ());
+var generator = new Generator (ctx, sampler);
+
+generator.generate_stream (prompt, max_tokens, (piece, token, is_final) =&gt; {
+    print ("%s", piece);
+    return true;      // false stops generation
+});</code></pre>
+            <figcaption>Condensed from <code>examples/simple.vala</code> — load a GGUF and generate.</figcaption>
+          </figure>
+          <h3>Tool calling, the headline feature</h3>
+          <p>Tools are declared with a small JSON-schema-ish API, compiled into a grammar that only accepts a valid call envelope (or a <code>respond</code> envelope carrying plain language):</p>
+          <figure class="code-plate">
+            <pre><code>var city = new ToolParam ("city", ToolParamType.STRING, true, "City name");
+var tool = new ToolSpec ("get_weather", "Get current weather", params);</code></pre>
+            <figcaption>From the README. The envelope: <code>{"name": "get_weather", "arguments": {"city": "Paris", "unit": "celsius"}}</code>.</figcaption>
+          </figure>
+          <p><code>ChatSession</code> keeps the transcript and runs the constrained loop:</p>
+          <figure class="code-plate">
+            <pre><code>var session = new ChatSession (model, ctx, sampler);
+session.add_tools (tools);
+session.add_message (new ChatMessage (ChatRole.SYSTEM, system_prompt));
+
+if (session.chat ("Weather in Paris?") == ChatTurnResult.TOOL_CALL) {
+    var call = session.last_tool_call;
+    string result = execute (call);           // your code
+    session.append_tool_result (call, result);
+    session.chat (null);                      // final prose turn
+    print ("%s\n", session.last_message_text ());
+}</code></pre>
+            <figcaption>Verbatim from the README. <code>run_with_tools (message, executor)</code> drives the whole loop via a callback.</figcaption>
+          </figure>
+          <p>The whole assistant turn stays inside one JSON envelope on purpose: GBNF cannot express "any text OR this JSON", so the <code>respond</code> pseudo-tool gives the model a clean escape hatch while every turn stays parseable. The parser accepts both the bare envelope and Qwen2.5-style <code>&lt;tool_call&gt;</code> markup.</p>
+          <h3>Caching the system prompt</h3>
+          <p>Pre-evaluate a long system prompt once and reuse the KV state — across turns in one run, or from a file in the next:</p>
+          <figure class="code-plate">
+            <pre><code>generator.prime (SYSTEM_PROMPT);        // evaluate once, keep KV state
+generator.save ("/path/to/cache.bin");  // persist it
+
+// a later run:
+generator.load ("/path/to/cache.bin");  // skip prompt evaluation,
+generator.respond ("Why is the sky blue?");</code></pre>
+            <figcaption><code>examples/cache.vala</code> primes, saves, restores and measures the speed-up end to end.</figcaption>
+          </figure>
+          <h3>Embeddings</h3>
+          <p>One text or many, L2-normalised, with the pooling mode the model wants:</p>
+          <figure class="code-plate">
+            <pre><code>var model = new Vallama.Model ("/path/Qwen3-Embedding-0.6B-Q8_0.gguf");
+var embedder = new Vallama.Embedder (model, Vallama.Pooling.LAST_TOKEN, ctx_opts);
+
+int dimensions = embedder.dimensions;      // model n_embd, e.g. 1024
+float[] query = embedder.embed ("Query: a city at the bottom of New Zealand");
+
+var texts = new Invercargill.DataStructures.Vector&lt;string&gt; ();
+texts.add ("Invercargill is the southernmost city in New Zealand.");
+float[] docs = embedder.embed_all (texts);          // flat row-major
+float[] doc0 = embedder.vector_at (docs, 0);        // slice row 0</code></pre>
+            <figcaption>From the README. Cosine similarity of two vectors is their dot product — every vector comes back normalised.</figcaption>
+          </figure>
+          <p>The test suite runs offline — <code>meson test</code> needs no model file: it pins the pooling mapping, normalisation maths, ChatML round-trips, envelope parsing (including malformed input) and template fallback selection, and validates the GBNF compiler's output through llama.cpp's own grammar parser. Live-model runs gate on <code>VALLAMA_TEST_MODEL=/path/to/model.gguf</code>; without the variable the examples print their API surface and exit cleanly.</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.0</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 vallama</code></dd></div>
+            <div><dt>Provides</dt><dd><code>libvallama-0.1.so</code>, <code>vallama-0.1.h</code> and <code>vallama-llama-shim.h</code>, <code>vallama-0.1.vapi</code> + <code>.deps</code> plus the <code>llama.vapi</code> binding in <code>share/vala/vapi</code>, <code>Vallama-0.1.gir</code>, <code>vallama-0.1.pc</code></dd></div>
+            <div><dt>Needs at runtime</dt><dd>GLib, GObject, <code>libinvercargill-1.so</code>, <code>libjson-glib-1.0.so</code>, and llama.cpp's <code>libllama.so</code>. A GGUF model file of your choosing.</dd></div>
+            <div><dt>Source</dt><dd>The repository <code>clanker/Vallama</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="Related in the catalogue">
+            <p>Its neighbours:</p>
+            <ul>
+              <li><a href="../invercargill/">Invercargill</a></li>
+              <li><a href="../locksmith/">Locksmith</a></li>
+              <li><a href="../usm/">USM</a></li>
+            </ul>
+          </nav>
+
+          <nav class="docs-pager" aria-label="Project pages">
+            <a class="docs-pager__link docs-pager__link--prev" href="../locksmith/">
+              <span>Previous</span>
+              <strong>Locksmith</strong>
+            </a>
+            <p class="docs-pager__up"><a href="../">All eleven</a></p>
+            <a class="docs-pager__link docs-pager__link--next" href="../usm/">
+              <span>Next in the catalogue</span>
+              <strong>USM</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>

+ 184 - 0
tools/qa-pages.py

@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+"""Headless QA for the three A5 pages: light/dark render + overflow at 390/768/1280.
+
+CDP per discovery #82: --force-prefers-color-scheme is silently ignored, so
+Emulation.setEmulatedMedia is the only reliable scheme flip. Metrics are set
+BEFORE navigate so layout never happens at another width; uncontained <code>
+inside <pre> is filtered when hunting real page-level overflow.
+
+Works over the browser endpoint (Target.createTarget + flatten sessions),
+which sidesteps the flaky /json/new page-target websocket on this Chromium.
+"""
+import json
+import subprocess
+import sys
+import time
+import urllib.request
+
+import websocket
+
+PORT = 9333
+PAGES = [
+    ("locksmith", "developer/locksmith/index.html"),
+    ("vallama", "developer/vallama/index.html"),
+    ("usm", "developer/usm/index.html"),
+]
+WIDTHS = [390, 768, 1280]
+SCHEMES = ["light", "dark"]
+
+def http_put(url):
+    req = urllib.request.Request(url, method="PUT", data=b"")
+    return json.load(urllib.request.urlopen(req))
+
+class Cdp:
+    def __init__(self, ws):
+        self.ws = ws
+        self.mid = 0
+
+    def call(self, method, params=None, timeout=30):
+        self.mid += 1
+        mid = self.mid
+        self.ws.send(json.dumps({"id": mid, "method": method,
+                                 "params": params or {}}))
+        deadline = time.time() + timeout
+        while time.time() < deadline:
+            try:
+                msg = json.loads(self.ws.recv())
+            except websocket.WebSocketTimeoutException:
+                continue
+            if msg.get("id") == mid:
+                if "error" in msg:
+                    raise RuntimeError(f"{method}: {msg['error']}")
+                return msg.get("result", {})
+        raise TimeoutError(method)
+
+    def eval(self, expr):
+        r = self.call("Runtime.evaluate",
+                      {"expression": expr, "returnByValue": True})
+        return r.get("result", {}).get("value")
+
+def main():
+    proc = subprocess.Popen([
+        "chromium-browser", "--headless=new", "--disable-gpu", "--no-sandbox",
+        "--remote-allow-origins=*", f"--remote-debugging-port={PORT}",
+        "about:blank",
+    ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+    try:
+        ver = None
+        for _ in range(50):
+            try:
+                ver = json.load(urllib.request.urlopen(
+                    f"http://127.0.0.1:{PORT}/json/version"))
+                break
+            except Exception:
+                time.sleep(0.2)
+        assert ver, "chromium did not start"
+        ws = websocket.create_connection(
+            ver["webSocketDebuggerUrl"],
+            origin=f"http://127.0.0.1:{PORT}", timeout=5)
+        ws.settimeout(0.5)
+        cdp = Cdp(ws)
+
+        failures = []
+        bg_by_page = {}
+        for name, path in PAGES:
+            url = "file://" + sys.argv[1].rstrip("/") + "/" + path
+            tid = cdp.call("Target.createTarget", {"url": "about:blank"})["targetId"]
+            sess = cdp.call("Target.attachToTarget",
+                            {"targetId": tid, "flatten": True})["sessionId"]
+
+            def send(method, params=None):
+                cdp.mid += 1
+                mid = cdp.mid
+                ws.send(json.dumps({"id": mid, "method": method,
+                                    "params": params or {},
+                                    "sessionId": sess}))
+                deadline = time.time() + 30
+                while time.time() < deadline:
+                    try:
+                        msg = json.loads(ws.recv())
+                    except websocket.WebSocketTimeoutException:
+                        continue
+                    if msg.get("id") == mid:
+                        if "error" in msg:
+                            raise RuntimeError(f"{method}: {msg['error']}")
+                        return msg.get("result", {})
+                raise TimeoutError(method)
+
+            send("Page.enable")
+            send("Emulation.setDeviceMetricsOverride",
+                 {"width": 390, "height": 900, "deviceScaleFactor": 1,
+                  "mobile": True})
+            send("Page.navigate", {"url": url})
+            time.sleep(1.5)  # fonts + layout settle
+
+            for scheme in SCHEMES:
+                send("Emulation.setEmulatedMedia",
+                     {"features": [{"name": "prefers-color-scheme",
+                                    "value": scheme}]})
+                for w in WIDTHS:
+                    send("Emulation.setDeviceMetricsOverride",
+                         {"width": w, "height": 900, "deviceScaleFactor": 1,
+                          "mobile": w < 500})
+                    time.sleep(0.3)
+                    data = json.loads(send("Runtime.evaluate", {
+                        "expression": """(() => {
+                            const de = document.documentElement;
+                            const over = [];
+                            const w = de.clientWidth;
+                            for (const el of document.querySelectorAll('body *')) {
+                                if (el.closest('pre')) continue;
+                                const b = el.getBoundingClientRect();
+                                if (b.width && (b.right > w + 1 || b.left < -1)) {
+                                    over.push(el.tagName + '.' + (el.className||''));
+                                }
+                            }
+                            return JSON.stringify({
+                                scrollW: de.scrollWidth, clientW: w,
+                                bg: getComputedStyle(document.body).backgroundColor,
+                                over: over.slice(0, 5),
+                                imgsBroken: [...document.images]
+                                    .filter(i => !i.complete || i.naturalWidth === 0)
+                                    .map(i => i.getAttribute('src')),
+                            });
+                        })()""", "returnByValue": True})
+                        ["result"]["value"])
+                    label = f"{name}/{scheme}/{w}"
+                    problems = []
+                    if data["scrollW"] > data["clientW"] + 1:
+                        problems.append(
+                            f"scroll {data['scrollW']}>{data['clientW']}")
+                    if data["over"]:
+                        problems.append("overflow: " + ",".join(data["over"]))
+                    if data["imgsBroken"]:
+                        problems.append(
+                            "broken imgs: " + ",".join(data["imgsBroken"]))
+                    if problems:
+                        failures.append(label + ": " + "; ".join(problems))
+                        print(f"FAIL {label}: {'; '.join(problems)}",
+                              file=sys.stderr)
+                    else:
+                        print(f"ok   {label} bg={data['bg']} "
+                              f"{data['scrollW']}/{data['clientW']}")
+                    bg_by_page.setdefault((name, w), {})[scheme] = data["bg"]
+
+            cdp.call("Target.closeTarget", {"targetId": tid})
+
+        # scheme sanity: bg must differ between light and dark
+        for (name, w), bgs in sorted(bg_by_page.items()):
+            if bgs.get("light") == bgs.get("dark"):
+                failures.append(f"{name}@{w}: light/dark bg identical "
+                                f"({bgs.get('light')}) — scheme flip failed")
+
+        if failures:
+            print(f"\n{len(failures)} failure(s)", file=sys.stderr)
+            for f in failures:
+                print(" -", f, file=sys.stderr)
+            return 1
+        print("\nALL OK (light/dark differ, no overflow, no broken images)")
+        return 0
+    finally:
+        proc.terminate()
+
+if __name__ == "__main__":
+    sys.exit(main())