From fd0658cde8e16029d4ee5e17bea13e9d6ef3631a Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:19:48 +0100 Subject: [PATCH 01/20] feat(provisioning): estate provisioning standard + launcher v0.5 modes Add 3-practice/provisioning/: the Provisioning Standard (prose + praxis deed), the engine (provision.just, provision-lib.sh, provision-modes.sh, provision-check.sh), and the minted templates (launcher, Justfile module, mise, guix source/cargo packages + manifest + channels, SETUP, AI install guide, three llm-warmups, README ai-install fragment, per-repo provisioning_praxis.deed). Launcher standard 0.5.0: every repository carries launcher.sh, profiled by archetype; --setup/--doctor/--heal/--ai-setup call the engine directly so a repo's own root recipe cannot shadow the canon; repo checks live in *-local recipes and a failing doctor-local is FAIL PV-E50. guix.scm: the licence field was a malformed ad-hoc license object pointing at palimpsest-license; it is now (guix licenses) mpl2.0, the licence the file's own SPDX header already declares. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../provisioning/PROVISIONING-STANDARD.adoc | 157 +++++ .../provisioning-standard_praxis.deed | 116 ++++ .../provisioning_praxis.deed.tmpl | 31 + .../provisioning/templates/Justfile.tmpl | 17 + .../templates/README-ai-install.adoc.tmpl | 65 ++ .../templates/build/just/provision-check.sh | 115 ++++ .../templates/build/just/provision-lib.sh | 554 ++++++++++++++++++ .../templates/build/just/provision-modes.sh | 138 +++++ .../templates/build/just/provision.just | 95 +++ .../docs/AI_INSTALLATION_GUIDE.adoc.tmpl | 134 +++++ .../templates/docs/SETUP.adoc.tmpl | 198 +++++++ .../provisioning/templates/guix/channels.scm | 18 + .../templates/guix/guix.scm.cargo.tmpl | 37 ++ .../templates/guix/guix.scm.source.tmpl | 40 ++ .../templates/guix/manifest.scm.tmpl | 14 + .../provisioning/templates/launcher.sh.tmpl | 42 ++ .../templates/llm-warmup-dev.adoc.tmpl | 49 ++ .../templates/llm-warmup-maintainer.adoc.tmpl | 42 ++ .../templates/llm-warmup-user.adoc.tmpl | 38 ++ .../provisioning/templates/mise.toml.tmpl | 14 + docs/UX-standards/launcher-standard.adoc | 89 +++ guix.scm | 2 +- launcher/launcher-standard_praxis.deed | 45 +- 23 files changed, 2046 insertions(+), 4 deletions(-) create mode 100644 3-practice/provisioning/PROVISIONING-STANDARD.adoc create mode 100644 3-practice/provisioning/provisioning-standard_praxis.deed create mode 100644 3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl create mode 100644 3-practice/provisioning/templates/Justfile.tmpl create mode 100644 3-practice/provisioning/templates/README-ai-install.adoc.tmpl create mode 100755 3-practice/provisioning/templates/build/just/provision-check.sh create mode 100755 3-practice/provisioning/templates/build/just/provision-lib.sh create mode 100755 3-practice/provisioning/templates/build/just/provision-modes.sh create mode 100644 3-practice/provisioning/templates/build/just/provision.just create mode 100644 3-practice/provisioning/templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl create mode 100644 3-practice/provisioning/templates/docs/SETUP.adoc.tmpl create mode 100644 3-practice/provisioning/templates/guix/channels.scm create mode 100644 3-practice/provisioning/templates/guix/guix.scm.cargo.tmpl create mode 100644 3-practice/provisioning/templates/guix/guix.scm.source.tmpl create mode 100644 3-practice/provisioning/templates/guix/manifest.scm.tmpl create mode 100755 3-practice/provisioning/templates/launcher.sh.tmpl create mode 100644 3-practice/provisioning/templates/llm-warmup-dev.adoc.tmpl create mode 100644 3-practice/provisioning/templates/llm-warmup-maintainer.adoc.tmpl create mode 100644 3-practice/provisioning/templates/llm-warmup-user.adoc.tmpl create mode 100644 3-practice/provisioning/templates/mise.toml.tmpl diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc new file mode 100644 index 000000000..98f9cb487 --- /dev/null +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += Provisioning Standard +:toc: +:toclevels: 2 +:revdate: 2026-09-30 +:revnumber: 1.0.0 + +*Question this answers:* what must every repository carry so that nobody who +clones it ever has to search for how to install, configure, run, test, benchmark, +diagnose or repair it? + +Machine-readable counterpart: link:provisioning-standard_praxis.deed[provisioning-standard_praxis.deed]. +Templates: link:templates/[templates/]. Launcher modes: `docs/UX-standards/launcher-standard.adoc` +§Provisioning modes (v0.5.0). + +== 1. The provisioning set + +Every repository in `hyperpolymath/*` and `metadatastician/*` carries the files +below, except forks, archived repositories, `007` and the two vaults +(`dev-notes-vault`, `memory-vault`). + +[cols="3,1,4"] +|=== +|File |Kind |Purpose + +|`launcher.sh` |engine |`--setup`, `--doctor`, `--heal`, `--ai-setup`, plus runtime modes for the `app` archetype +|`build/just/provision.just` |engine |the `provision::` just module: every verb below +|`build/just/provision-lib.sh` |engine |the implementation of every verb (bash only, shellcheck-clean) +|`build/just/provision-modes.sh` |engine |the launcher's provisioning dispatch +|`channels.scm` |engine |the pinned Guix commit +|`Justfile` |merged |`mod provision` plus root delegations; the repo's own recipes are kept +|`mise.toml` + `mise.lock` |minted |the toolchain: `latest` in `mise.toml`, made concrete and checksummed in `mise.lock` +|`guix.scm`, `manifest.scm` |minted |the Guix package and development shell +|`.machine_readable/descriptiles/provisioning_praxis.deed` |minted |this repo's facts: archetype, languages, verb overrides, system deps, ports, config +|`docs/SETUP.adoc` |minted |the complete manual route, with the doctor-code troubleshooting table +|`docs/AI_INSTALLATION_GUIDE.adoc` |minted |the steps an AI assistant follows +|`llm-warmup-{user,dev,maintainer}.adoc` |minted |paste-in context for any AI, one per audience +|README `[[ai-install]]` section |inserted |"Just say it": one sentence to give any AI +|=== + +*Engine* files are identical estate-wide and `provision-set realign` overwrites +them. *Minted* files are written once, then owned by the repository: realign +creates them when missing and replaces them only while they are still stubs +(doctor PV-W24–W28), never once filled. The Justfile is merged: an existing +custom `doctor`, `setup` or `heal` recipe is renamed `doctor-local`, +`setup-local` or `heal-local`, and the canon verbs run it (§3). + +== 2. The verbs + +`just ` and `just provision::` are the same. `./launcher.sh --setup`, +`--doctor`, `--heal` and `--ai-setup` call the engine directly, so a root recipe +of the same name can never shadow them. + +[cols="1,4"] +|=== +|Verb |Contract + +|`setup` |`mise install`, the repo's dependencies per language, `setup-local`, then `doctor` +|`doctor` |PASS / WARN / FAIL for every requirement, each with a code and its fix; the last line is `: N PASS, N WARN, N FAIL`; exits non-zero if and only if something FAILed +|`heal` |applies every fix marked *auto*, runs `heal-local`, then `doctor` +|`dev-shell` |`guix shell -m manifest.scm` when Guix is present, otherwise the mise environment +|`toolchain-refresh` |`mise up --bump`, re-lock `mise.lock`, re-pin `channels.scm` (the TRUST-DEFAULTS-POLICY recipe) +|`build`, `test`, `bench`, `lint`, `fmt`, `run`, `deps` |the deed's override, else the language default, else an explicit "N/A for this archetype" and exit 0; nothing is faked +|`eval` |`test` + `bench` with timings, saved under `.eval/` +|`config-show` |where the toolchain and configuration are declared +|`ai-setup` |prints the "Just say it" sentence and the guide path +|`ai-warmup ` |prints that warm-up for pasting into any AI +|`opsm` |how to fetch this repository with OPSM, and how to get OPSM +|`langs`, `search`, `version` |discovery helpers +|=== + +Archetypes (`app`, `library`, `tool`, `theory`, `docs`) come from the deed. Only +`app` has runtime modes (start/stop/status); the others say N/A. + +== 3. Repository-specific checks + +A repository keeps its own checks as root recipes named `doctor-local`, +`setup-local` and `heal-local`, or as scripts `build/just/{doctor,setup,heal}-local.sh`. +The canon verbs run both. A failing `doctor-local` is a FAIL (PV-E50), so it turns +`just doctor`, `./launcher.sh --doctor` and CI red together. + +The estate's pre-existing boilerplate doctors (the `rsr-diag` and +`toolchain-check` families, 326 of 346 measured on 2026-09-30) print `[FAIL]` +and exit 0; they are replaced by `doctor: provision::doctor`, not renamed. + +== 4. Toolchain + +* *mise*: tools at `latest`, `[settings] lockfile = true`; `mise.lock` pins each + to a version and per-platform checksum. The base set is `just` and `shellcheck`; + each detected language adds its own (rust, zig, julia, erlang+elixir, + erlang+gleam, opam, bun, lychee for docs). Banned tools (python, deno, node, + npm, yarn, go, java, make) never appear. +* *Guix*: `channels.scm` pins the commit; `manifest.scm` lists the development + shell. Rust repositories get a `cargo-build-system` `guix.scm`; every other + ecosystem gets a *source* package (`copy-build-system`) that says so, because a + hermetic compiled build needs that ecosystem's dependencies packaged in Guix, + which they are not. A spec may name an output (`rust:cargo`). + At commit ae77aeb Guix does not package idris2, gleam, bun or lychee. For those + tools the Guix shell provides mise. +* *just ≥ 1.42*: a root recipe depending on a module recipe + (`doctor: provision::doctor`) fails on 1.31, 1.36, 1.40 and 1.41 and works from + 1.42 (measured 2026-09-30). Doctor enforces the floor as PV-E02. +* *bun* is the only JavaScript runtime. Leftover deno, Nix, Makefile, Python or + npm files are WARNs (PV-W30–W34), not FAILs. Each deno repository gets one + migration issue. + +== 5. Template slots + +Templates carry two tiers of placeholder: + +* `+__X__+`: mechanical, filled by the generator from evidence (name, slug, + languages, tools, licence, copyright holder). +* `+__SPEC_X__+`: repository-specific prose (what it is, the questions to ask, + privacy notice, verification, usage, uninstall, architecture, gotchas, + release, CI). This is written by reading the repository. + +`provision-set check` treats any `+__X__+` residue as a hard failure. `+__SPEC_X__+` +residue is a hard failure at the pre-PR gate. In a development checkout, doctor +reports it as PV-W29 instead. AsciiDoc renders a stray `+__X__+` as italics, so +residue is visible as garbled text. + +The pipeline order is fixed: *mint → specialise → verify → PR*. + +== 6. Licence of minted files + +Minted files carry an SPDX header from birth. That is authoring, not +relicensing. The header matches the repository's classification in +`3-practice/LICENCE-POLICY.adoc`: + +* code and config (`+__LICENSE__+`) are MPL-2.0 for sole-owner repos, AGPL-3.0-or-later for the son-shared repos, and PMPL-1.0-or-later only for the register; +* prose docs (`+__DOC_LICENSE__+`) are CC-BY-SA-4.0 in sole-owner repos. + +When the generator cannot classify a repository from evidence, it refuses to mint +and records the repository in the campaign ledger. It never guesses, never +rewrites an existing header, and never touches forks. The engine files are +estate-wide and stay MPL-2.0 wherever they are vendored. + +== 7. OPSM + +Every `docs/SETUP.adoc` has an OPSM section with +`opsm install https://github.com/.git --registry git` and the steps to build +OPSM itself. Repositories without a registry package are fetched through OPSM's +git adapter, and the section says so. + +== 8. Conformance + +A repository conforms when, on a fresh clone: + +. `./launcher.sh --help` and `--version` exit 0; +. `just --list` parses and lists every verb in §2; +. `mise.toml` names no banned tool, and `mise.lock` exists; +. `guix.scm` is not a stub; +. no template residue remains (§5); +. `just doctor` exits 0 once `just setup` has run. + +`templates/build/just/provision-check.sh` checks items 1–5 without network access. diff --git a/3-practice/provisioning/provisioning-standard_praxis.deed b/3-practice/provisioning/provisioning-standard_praxis.deed new file mode 100644 index 000000000..73ee49396 --- /dev/null +++ b/3-practice/provisioning/provisioning-standard_praxis.deed @@ -0,0 +1,116 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: MPL-2.0 +;; +;; provisioning-standard_praxis.deed — what every repository carries so that +;; nobody who clones it has to search for how to set it up, run it, test it, +;; diagnose it or repair it. +;; +;; Prose counterpart: 3-practice/provisioning/PROVISIONING-STANDARD.adoc +;; Launcher modes: launcher/launcher-standard_praxis.deed (provisioning-modes) +;; Templates: 3-practice/provisioning/templates/ +;; Offline checker: templates/build/just/provision-check.sh +(praxis-deed + :schema-version "1.0.0" + :canonical-name "provisioning-standard" + :beholding-chora #u5"estate/chora" + :standard-version "1.0.0" + :standard-date "2026-09-30" + :compliance ("PROVISIONING-STANDARD.adoc" "launcher-standard.adoc" + "TRUST-DEFAULTS-POLICY.adoc" "LICENCE-POLICY.adoc") + + ;; ------------------------------------------------------------------ scope + (scope + :orgs ("hyperpolymath" "metadatastician") + :exclude ("forks" "archived" "007" "dev-notes-vault" "memory-vault")) + + ;; ------------------------------------------------------------ artefact set + ;; engine = identical estate-wide; `provision-set realign` overwrites it. + ;; minted = written once, then owned by the repository; realign creates it + ;; when missing and replaces it only while it is still a stub. + ;; merged = the repository's own content is kept; the canon is added. + (artefacts + (file :path "launcher.sh" :kind engine :mode "100755") + (file :path "build/just/provision.just" :kind engine) + (file :path "build/just/provision-lib.sh" :kind engine :mode "100755") + (file :path "build/just/provision-modes.sh" :kind engine :mode "100755") + (file :path "build/just/provision-check.sh" :kind engine :mode "100755") + (file :path "channels.scm" :kind engine) + (file :path "Justfile" :kind merged) + (file :path "mise.toml" :kind minted) + (file :path "mise.lock" :kind minted) + (file :path "guix.scm" :kind minted) + (file :path "manifest.scm" :kind minted) + (file :path ".machine_readable/descriptiles/provisioning_praxis.deed" :kind minted) + (file :path "docs/SETUP.adoc" :kind minted) + (file :path "docs/AI_INSTALLATION_GUIDE.adoc" :kind minted) + (file :path "llm-warmup-user.adoc" :kind minted) + (file :path "llm-warmup-dev.adoc" :kind minted) + (file :path "llm-warmup-maintainer.adoc" :kind minted) + (section :file "README.adoc" :anchor "ai-install" :kind merged)) + + ;; ------------------------------------------------------------------- verbs + ;; Every verb exists both as a root recipe and as provision::. + ;; Launcher modes call the engine directly, so no root recipe can shadow them. + (verbs + :provisioning ("setup" "doctor" "heal" "dev-shell" "toolchain-refresh") + :lifecycle ("build" "test" "bench" "eval" "lint" "fmt" "run" "deps") + :guidance ("ai-setup" "ai-warmup" "config-show" "opsm")) + (launcher-modes + (mode :flag "--setup" :verb "setup") + (mode :flag "--doctor" :verb "doctor") + (mode :flag "--heal" :verb "heal") + (mode :flag "--ai-setup" :verb "ai-setup")) + ;; A repository's own checks: root recipes -local, or scripts + ;; build/just/-local.sh. A failing doctor-local is FAIL PV-E50. + (local-hooks :verbs ("doctor" "setup" "heal") :suffix "-local") + (not-applicable :behaviour "print why and exit 0; never fake a result") + + (archetypes + (archetype :name app :runtime-modes #t) + (archetype :name library :runtime-modes #f) + (archetype :name tool :runtime-modes #f) + (archetype :name theory :runtime-modes #f) + (archetype :name docs :runtime-modes #f)) + + ;; --------------------------------------------------------------- toolchain + (toolchain + :mise-versions "latest" + :mise-lockfile #t + :mise-base ("just" "shellcheck") + :js-runtime "bun" + :banned-tools ("python" "deno" "node" "npm" "yarn" "pnpm" "go" "java" "kotlin" "make") + ;; Measured 2026-09-30: a root recipe depending on a module recipe fails on + ;; just 1.31, 1.36, 1.40, 1.41 and works from 1.42. + :just-floor "1.42.0" + :guix-channels "channels.scm" + :guix-package (rust "cargo-build-system" other "copy-build-system source package") + :guix-gaps ("idris2" "gleam" "bun" "lychee")) + + ;; ------------------------------------------------------------- slot tiers + (slots + (tier :name mechanical :pattern "__X__" :filled-by "provision-set mint" + :residue fail) + (tier :name specific :pattern "__SPEC_X__" :filled-by "per-repository specialisation" + :residue fail :residue-in-dev-checkout "warn PV-W29")) + (pipeline :order ("mint" "specialise" "verify" "pr")) + + ;; ----------------------------------------------------------------- licence + ;; Minted files carry SPDX from birth, chosen by the repository's + ;; classification in LICENCE-POLICY.adoc. Unclassifiable = refuse to mint. + (licence + :code-slot "__LICENSE__" + :doc-slot "__DOC_LICENSE__" + :sole ("MPL-2.0" "CC-BY-SA-4.0") + :son-shared ("AGPL-3.0-or-later" "AGPL-3.0-or-later") + :register-only "PMPL-1.0-or-later" + :unclassifiable "refuse and record in the campaign ledger" + :existing-headers "never rewritten") + + ;; ----------------------------------------------------------- doctor codes + ;; Full table: templates/docs/SETUP.adoc.tmpl §Troubleshooting. + (doctor + :summary-line "{name}: N PASS, N WARN, N FAIL" + :exit "non-zero iff any FAIL" + :fail-codes ("PV-E01" "PV-E02" "PV-E03" "PV-E10" "PV-E11" "PV-E20" "PV-E27" "PV-E40" "PV-E50") + :warn-codes ("PV-W01" "PV-W20" "PV-W21" "PV-W22" "PV-W23" "PV-W24" "PV-W25" "PV-W26" + "PV-W27" "PV-W28" "PV-W29" "PV-W30" "PV-W31" "PV-W32" "PV-W33" "PV-W34"))) diff --git a/3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl b/3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl new file mode 100644 index 000000000..d10d736ca --- /dev/null +++ b/3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl @@ -0,0 +1,31 @@ +;; SPDX-License-Identifier: __LICENSE__ +;; SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ +;; +;; provisioning_praxis.deed: the repo-specific facts behind `just setup/doctor/run/…`. +;; build/just/provision-lib.sh reads each `:key "value"` field below. Rules: +;; * one key per line, value in double quotes, and no double quote inside a value +;; (put anything that needs quoting in build/just/setup-local.sh or a script); +;; * an empty value "" means "use the per-language default"; +;; * a verb key (build test bench lint fmt run deps) replaces the default command. +;; Canon: hyperpolymath/standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc +(praxis-deed + :schema-version "1.0.0" + :canonical-name "__APP_NAME__-provisioning" + :repo "__REPO_SLUG__" + :archetype "__ARCHETYPE__" ; app | library | tool | theory | docs + :languages "__LANGS__" ; detected; informational + ;; Contract-verb overrides ("" = per-language default, which `just langs` shows) + :build "" + :test "" + :bench "" + :lint "" + :fmt "" + :run "" + :deps "" + ;; Facts shown by `just config-show` and checked by `just doctor` + :config "" ; where runtime configuration lives + :ports "" ; ports an app listens on (estate: no 8080-class) + :system-deps "" ; OS commands that must exist, space-separated + ;; What `just ai-setup` and `just opsm` print ("" = the generic line) + :ai-say-it "" + :opsm-note "") diff --git a/3-practice/provisioning/templates/Justfile.tmpl b/3-practice/provisioning/templates/Justfile.tmpl new file mode 100644 index 000000000..893b98c01 --- /dev/null +++ b/3-practice/provisioning/templates/Justfile.tmpl @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: __LICENSE__ +# SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ +# +# __APP_NAME__ — every task is a recipe here; `just` lists them. +# The provisioning contract (setup doctor heal eval dev-shell toolchain-refresh +# ai-setup ai-warmup config-show opsm) comes from build/just/provision.just, and +# the per-language defaults for build/test/bench/lint/fmt/run from the same +# module. Define a recipe here to override one for this repository. +# Needs just >= 1.42 (a root recipe depending on a module recipe, e.g. `doctor: provision::doctor`). + +mod provision 'build/just/provision.just' + +# List every recipe +default: + @just --list --list-submodules + +__DELEGATIONS__ diff --git a/3-practice/provisioning/templates/README-ai-install.adoc.tmpl b/3-practice/provisioning/templates/README-ai-install.adoc.tmpl new file mode 100644 index 000000000..ddc6d1bf5 --- /dev/null +++ b/3-practice/provisioning/templates/README-ai-install.adoc.tmpl @@ -0,0 +1,65 @@ +[TIP] +==== +*AI-assisted install:* tell any AI assistant + +`Set up __APP_NAME__ from https://github.com/__REPO_SLUG__` + +It reads this repository, asks a few questions and does the rest. <>. +==== + +[[ai-install]] +== AI-Assisted Installation (Recommended) + +=== Just say it + +You do not need to read the rest of this README. Say this to any AI assistant +that can read a URL and run commands (or give you commands to paste): + +[source,text] +---- +Set up __APP_NAME__ from https://github.com/__REPO_SLUG__ +---- + +The URL is the key: it leads the assistant to +link:docs/AI_INSTALLATION_GUIDE.adoc[`docs/AI_INSTALLATION_GUIDE.adoc`], the +complete step-by-step recipe for this repository. The assistant checks your +system, installs the prerequisites, fetches and sets up __APP_NAME__, and verifies +it with `just doctor`. + +=== Other ways to say it + +* "Install https://github.com/__REPO_SLUG__ for me" +* "Get __APP_NAME__ working on my machine — https://github.com/__REPO_SLUG__" +* `./launcher.sh --ai-setup` (or `just ai-setup`) prints the line and copies it to your clipboard. + +=== What you will be asked + +. Your operating system (Linux, macOS, or Windows via WSL2). +. Where to put it (default `~/src/__APP_NAME__`). +. To confirm the privacy notice below. +__SPEC_QUESTIONS__ + +=== Privacy and security + +[IMPORTANT] +==== +Setting up __APP_NAME__ clones this repository and installs its developer tools +per-user with mise (no administrator rights, nothing sent anywhere). + +__SPEC_PRIVACY__ +==== + +=== After install + +__SPEC_USAGE__ + +=== Uninstall + +Tell your assistant "Uninstall __APP_NAME__", or delete the checkout +(`rm -rf ~/src/__APP_NAME__`). Shared tools are removed with `mise uninstall`. + +=== Troubleshooting + +Run `just doctor`; each FAIL carries a code explained in +link:docs/SETUP.adoc#troubleshooting[`docs/SETUP.adoc` §Troubleshooting], and +`just heal` applies the automatic fixes. Prefer to do it yourself? The full +manual route is link:docs/SETUP.adoc[`docs/SETUP.adoc`]; developers and +maintainers can brief an AI with `just ai-warmup dev` / `just ai-warmup maintainer`. diff --git a/3-practice/provisioning/templates/build/just/provision-check.sh b/3-practice/provisioning/templates/build/just/provision-check.sh new file mode 100755 index 000000000..1e438edcd --- /dev/null +++ b/3-practice/provisioning/templates/build/just/provision-check.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# provision-check.sh — offline conformance check for the provisioning set +# (PROVISIONING-STANDARD.adoc §8, items 1–5). No network, no installs. +# +# provision-check.sh [--dev] [REPO_DIR] +# +# Exit 0 = conforms, 1 = does not. Every failure is printed; none is skipped. +# --dev downgrades repository-specific slot residue (__SPEC_X__) to a warning, +# for a checkout mid-specialisation. Mechanical residue (__X__) always fails. +set -uo pipefail + +DEV=0 +[ "${1:-}" = "--dev" ] && { DEV=1; shift; } +REPO="${1:-.}" +cd "$REPO" || { echo "provision-check: no such directory: $REPO" >&2; exit 1; } + +MIN_JUST="1.42.0" +VERBS="setup doctor heal dev-shell toolchain-refresh ai-setup ai-warmup eval config-show opsm build test bench lint fmt run deps" +BANNED="python deno node nodejs npm yarn pnpm go golang java kotlin make black ruff" +# Files the provisioning set owns; residue anywhere else is not ours to judge. +# The engine scripts under build/just/ are excluded: they name slot syntax in their +# own comments and code, and realign owns them. README.adoc is scanned only inside +# its [[ai-install]] section. +SET_FILES="launcher.sh Justfile justfile mise.toml guix.scm manifest.scm channels.scm + .machine_readable/descriptiles/provisioning_praxis.deed docs/SETUP.adoc + docs/AI_INSTALLATION_GUIDE.adoc llm-warmup-user.adoc llm-warmup-dev.adoc + llm-warmup-maintainer.adoc" + +fails=0 warns=0 +ok() { printf ' ok %s\n' "$*"; } +bad() { printf ' FAIL %s\n' "$*"; fails=$((fails + 1)); } +warn() { printf ' warn %s\n' "$*"; warns=$((warns + 1)); } + +version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } + +echo "[1] launcher" +if [ ! -f launcher.sh ]; then bad "launcher.sh missing" +elif [ ! -x launcher.sh ]; then bad "launcher.sh is not executable (chmod +x; committed mode must be 100755)" +else + for m in --help --version; do + if ./launcher.sh "$m" >/dev/null 2>&1; then ok "launcher.sh $m exits 0" + else bad "launcher.sh $m exits non-zero"; fi + done +fi + +echo "[2] just" +f0=$fails +if ! command -v just >/dev/null 2>&1; then bad "just is not installed; cannot check the recipe contract" +else + jv=$(just --version 2>/dev/null | awk '{print $2}') + if version_ge "$jv" "$MIN_JUST"; then ok "just $jv >= $MIN_JUST" + else bad "just $jv < $MIN_JUST (module-recipe dependencies do not resolve)"; fi + if ! summary=$(just --summary 2>&1); then + bad "the Justfile does not parse: $(printf '%s' "$summary" | head -1)" + else + have=" $summary " + for v in $VERBS; do + case "$have" in *" $v "*) ;; *) bad "root recipe missing: $v" ;; esac + case "$have" in *" provision::$v "*) ;; *) bad "module recipe missing: provision::$v (engine drift)" ;; esac + done + [ "$fails" -eq "$f0" ] && ok "all ${VERBS// /, } present at root and in provision::" + fi +fi + +echo "[3] mise" +if [ ! -f mise.toml ]; then bad "mise.toml missing" +else + tools=$(awk '/^\[tools\]/{t=1;next} /^\[/{t=0} t && /=/{sub(/[ \t]*=.*/,""); gsub(/["\x27 ]/,""); print}' mise.toml) + hit="" + for t in $tools; do + base=${t##*:}; base=${base%%@*}; base=${base##*/} # "aqua:denoland/deno" -> deno + for b in $BANNED; do [ "$base" = "$b" ] && hit="$hit$t "; done + done + if [ -n "$hit" ]; then bad "mise.toml names banned tools: $hit"; else ok "mise.toml names no banned tool"; fi + [ -f .mise.toml ] && bad "both mise.toml and .mise.toml exist" + if [ -f mise.lock ]; then ok "mise.lock present"; else bad "mise.lock missing (latest is not concrete; run: mise lock)"; fi +fi + +echo "[4] guix" +if [ ! -f guix.scm ]; then bad "guix.scm missing" +elif grep -qE '\{\{|\(inputs \(list\)\)|\(source #f\)' guix.scm; then bad "guix.scm is a stub" +else ok "guix.scm is not a stub"; fi +for g in manifest.scm channels.scm; do [ -f "$g" ] || bad "$g missing"; done + +echo "[5] template residue" +f0=$fails +residue() { # $1 label; stdin = the text to judge + local text mech spec + text=$(cat) + # Mechanical slots (__X__ not starting SPEC_) are the generator's job: always fatal. + mech=$(printf '%s\n' "$text" | grep -noE '__[A-Z][A-Z_]*__' | grep -v ':__SPEC_' | head -3 | tr '\n' ' ') + spec=$(printf '%s\n' "$text" | grep -noE '__SPEC_[A-Z_]*__' | head -3 | tr '\n' ' ') + [ -n "$mech" ] && bad "$1: unfilled mechanical slots: $mech" + if [ -n "$spec" ]; then + if [ "$DEV" = 1 ]; then warn "$1: unfilled repository slots: $spec" + else bad "$1: unfilled repository slots: $spec"; fi + fi +} +for f in $SET_FILES; do + # shellcheck disable=SC2094 # $f is only the label; nothing writes it + [ -f "$f" ] && residue "$f" < "$f" +done +for r in README.adoc README.md; do + [ -f "$r" ] || continue + # From the [[ai-install]] anchor to the next level-2 heading after its own. + residue "$r [[ai-install]]" < <(awk '/^\[\[ai-install\]\]/{on=1; n=0} on && /^== /{n++; if (n>1) exit} on' "$r") +done +[ "$fails" -eq "$f0" ] && ok "no residue that fails this mode" + +echo +echo "provision-check: $fails FAIL, $warns WARN" +[ "$fails" -eq 0 ] diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh new file mode 100755 index 000000000..9eed95eca --- /dev/null +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -0,0 +1,554 @@ +#!/usr/bin/env bash +# SC2015: `A && pass || fail` is safe here — pass/warn/fail/info always return 0. +# shellcheck disable=SC2015 +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# provision-lib.sh — the shared provisioning engine behind build/just/provision.just +# +# Canon: hyperpolymath/standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc +# Vendored into every repo at build/just/provision-lib.sh by `provision-set mint`. +# DO NOT hand-edit a vendored copy: repo-specific facts live in +# .machine_readable/descriptiles/provisioning_praxis.deed, and `provision-set realign` +# overwrites this file from canon. +# +# Usage: provision-lib.sh [args] +# verbs: langs doctor setup heal dev-shell toolchain-refresh ai-setup +# ai-warmup eval config-show opsm lang-run +# search version guix-specs mise-tools +# +# Exit: 0 ok | 1 a FAIL was found / a step failed | 2 usage error +set -uo pipefail + +PROVISION_LIB_VERSION="0.5.0" +ROOT="${PROVISION_ROOT:-$(pwd)}" +cd "$ROOT" || exit 2 +DEED=".machine_readable/descriptiles/provisioning_praxis.deed" +MIN_JUST="1.42.0" # root→module recipe deps (`doctor: provision::doctor`); 1.41 rejects them +T="timeout 15" # some --version probes hang (observed 2026-09-30); never probe unbounded + +if [ -t 1 ]; then R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[34m'; Z=$'\033[0m'; else R=; G=; Y=; B=; Z=; fi +PASS=0; WARN=0; FAIL=0 +pass() { PASS=$((PASS+1)); printf ' %sPASS%s %s\n' "$G" "$Z" "$*"; } +warn() { WARN=$((WARN+1)); printf ' %sWARN%s %s\n' "$Y" "$Z" "$*"; } +fail() { FAIL=$((FAIL+1)); printf ' %sFAIL%s %s\n' "$R" "$Z" "$*"; } +info() { printf ' %sinfo%s %s\n' "$B" "$Z" "$*"; } +hdr() { printf '\n%s== %s ==%s\n' "$B" "$*" "$Z"; } +have() { command -v "$1" >/dev/null 2>&1; } + +# --------------------------------------------------------------------------- +# Descriptor: flat `:key "value"` reads from provisioning_praxis.deed (s-expression). +deed() { # $1 key, $2 default + local v="" + [ -f "$DEED" ] && v=$(grep -oE "\(:?$1[[:space:]]+\"[^\"]*\"" "$DEED" 2>/dev/null | head -1 | sed -E 's/^[^"]*"//; s/"$//') + [ -z "$v" ] && v=$(grep -oE ":$1[[:space:]]+\"[^\"]*\"" "$DEED" 2>/dev/null | head -1 | sed -E 's/^[^"]*"//; s/"$//') + printf '%s' "${v:-$2}" +} +repo_slug() { + local u; u=$(git config --get remote.origin.url 2>/dev/null || true) + u=${u%.git}; u=${u#*github.com[:/]} + [ -n "$u" ] && printf '%s' "$u" || printf 'hyperpolymath/%s' "$(basename "$ROOT")" +} +REPO_SLUG="$(deed repo "$(repo_slug)")" +REPO_NAME="${REPO_SLUG#*/}" +ARCHETYPE="$(deed archetype "")" + +# --------------------------------------------------------------------------- +# Language detection. Markers at the root or one level down (workspaces). +# Order matters only for display. `docs` is reported when nothing else is. +first() { find . -maxdepth "${2:-2}" -not -path './.git/*' -not -path '*/node_modules/*' -not -path './target/*' -name "$1" -print -quit 2>/dev/null; } +detect_langs() { + local out=() + [ -n "$(first Cargo.toml)" ] && out+=(rust) + [ -n "$(first '*.ipkg')" ] && out+=(idris2) + [ -n "$(first Project.toml 1)" ] && out+=(julia) + [ -n "$(first build.zig)" ] && out+=(zig) + [ -n "$(first mix.exs)" ] && out+=(elixir) + [ -n "$(first gleam.toml)" ] && out+=(gleam) + [ -n "$(first dune-project)" ] && out+=(ocaml) + { [ -n "$(first '*.cabal')" ] || [ -n "$(first stack.yaml 1)" ]; } && out+=(haskell) + [ -n "$(first package.json)" ] && out+=(bun) + [ ${#out[@]} -eq 0 ] && out+=(docs) + printf '%s\n' "${out[@]}" +} +mapfile -t LANGS < <(detect_langs) +[ -z "$ARCHETYPE" ] && { [ "${LANGS[*]}" = docs ] && ARCHETYPE=docs || ARCHETYPE=library; } + +# Binaries each language needs, and how each is obtained. +# mise registry gaps (verified 2026-09-30 against mise 2026.7.5): ocaml, haskell, +# idris2 and guile are NOT mise tools. They come from opam / ghcup / pack / the +# system package manager (or Guix), and the doctor probes the binary itself. +lang_tools() { + case "$1" in + rust) echo "cargo rustc" ;; + idris2) echo "idris2" ;; + julia) echo "julia" ;; + zig) echo "zig" ;; + elixir) echo "elixir mix erl" ;; + gleam) echo "gleam erl" ;; + ocaml) echo "opam dune ocaml" ;; + haskell) echo "ghc cabal" ;; + bun) echo "bun" ;; + docs) echo "" ;; + esac +} +# Guix package specs per language (verified 2026-09-30 against guix ae77aeb in +# docker.io/metacall/guix). Guix has NO idris2, gleam, bun or lychee, and its +# julia is 1.8.5: those come from mise, which Guix itself ships ("mise"). +GUIX_BASE="git bash coreutils nss-certs just mise shellcheck" +lang_guix() { + case "$1" in + rust) echo "rust rust:cargo gcc-toolchain pkg-config" ;; + idris2) echo "chez-scheme gmp gcc-toolchain" ;; + zig) echo "zig" ;; + elixir) echo "elixir erlang" ;; + gleam) echo "erlang" ;; + ocaml) echo "ocaml dune opam ocaml-findlib" ;; + haskell) echo "ghc cabal-install" ;; + docs) echo "ruby-asciidoctor" ;; + julia|bun) echo "" ;; + esac +} +# Tools a Guix shell cannot supply, so mise (inside that shell) does. +lang_guix_gap() { + case "$1" in + idris2) echo "idris2 (via pack)" ;; julia) echo julia ;; gleam) echo gleam ;; bun) echo bun ;; docs) echo lychee ;; + esac +} +guix_specs() { + local l s=" $GUIX_BASE " + for l in "${LANGS[@]}"; do for p in $(lang_guix "$l"); do case "$s" in *" $p "*) ;; *) s="$s$p ";; esac; done; done + echo "$s" | xargs +} + +# mise tools per language (registry-checked 2026-09-30, mise 2026.7.5). idris2 comes +# from pack and haskell from ghcup or Guix: neither is a mise tool. The recipes +# themselves need just and shellcheck, so those two are always declared. +MISE_BASE="just shellcheck" +lang_mise() { case "$1" in + rust) echo "rust" ;; zig) echo "zig" ;; julia) echo "julia" ;; + elixir) echo "erlang elixir" ;; gleam) echo "erlang gleam" ;; ocaml) echo "opam" ;; + bun) echo "bun" ;; docs) echo "lychee" ;; esac; } +mise_tools() { + local l t seen=" " out=() + for t in $MISE_BASE $(for l in "${LANGS[@]}"; do lang_mise "$l"; done); do + case "$seen" in *" $t "*) ;; *) out+=("$t"); seen="$seen$t " ;; esac + done + printf "%s\n" "${out[*]}" +} + +lang_remedy() { + case "$1" in + rust) echo "mise use rust@latest (or: curl https://sh.rustup.rs | sh)" ;; + idris2) echo "install pack: https://github.com/stefan-hoeck/idris2-pack#installation, then: pack install-app idris2" ;; + julia) echo "mise use julia@latest (or: https://julialang.org/install/ — juliaup)" ;; + zig) echo "mise use zig@latest" ;; + elixir) echo "mise use erlang@latest elixir@latest (erlang builds from source: allow ~10 min)" ;; + gleam) echo "mise use gleam@latest erlang@latest" ;; + ocaml) echo "mise use opam@latest && opam init -y && opam switch create . --deps-only -y" ;; + haskell) echo "ghcup: https://www.haskell.org/ghcup/ (or: guix shell -m manifest.scm)" ;; + bun) echo "mise use bun@latest" ;; + esac +} + +# The per-language default for each contract verb. A repo overrides any of +# these simply by defining the root recipe itself; `provision-set` only adds +# `: provision::` where the root Justfile has no such recipe. +lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) + local l=$1 v=$2 ipkg + case "$l:$v" in + rust:deps) echo "cargo fetch" ;; + rust:build) echo "cargo build --all-targets" ;; + rust:test) echo "cargo test --all-targets" ;; + rust:bench) grep -rqs '\[\[bench\]\]\|criterion\|divan' --include=Cargo.toml . && echo "cargo bench" ;; + rust:lint) echo "cargo clippy --all-targets -- -D warnings" ;; + rust:fmt) echo "cargo fmt --all" ;; + rust:run) echo "cargo run --release" ;; + + idris2:*) + ipkg=$(first '*.ipkg' 1); ipkg=${ipkg#./}; [ -z "$ipkg" ] && ipkg=$(first '*.ipkg'); ipkg=${ipkg#./} + case "$v" in + deps) have pack && echo "pack install-deps $ipkg" ;; + build) have pack && echo "pack build $ipkg" || echo "idris2 --build $ipkg" ;; + test) local t; t=$(find . -maxdepth 3 -name 'test*.ipkg' -print -quit 2>/dev/null) + if [ -n "$t" ]; then have pack && echo "pack test ${t#./}" || echo "idris2 --build ${t#./}" + else have pack && echo "pack typecheck $ipkg" || echo "idris2 --typecheck $ipkg"; fi ;; + run) grep -qs '^[[:space:]]*executable' "$ipkg" && { have pack && echo "pack run $ipkg" || echo "idris2 --build $ipkg && ./build/exec/*"; } ;; + esac ;; + + julia:deps) echo "julia --project=. -e 'using Pkg; Pkg.instantiate()'" ;; + julia:build) echo "julia --project=. -e 'using Pkg; Pkg.precompile()'" ;; + julia:test) echo "julia --project=. -e 'using Pkg; Pkg.test()'" ;; + julia:bench) [ -f benchmark/benchmarks.jl ] && echo "julia --project=benchmark -e 'using Pkg; Pkg.develop(path=\".\"); Pkg.instantiate(); include(\"benchmark/benchmarks.jl\")'" ;; + + zig:build) echo "zig build" ;; + zig:test) echo "zig build test" ;; + zig:bench) grep -qs '"bench"' build.zig && echo "zig build bench -Doptimize=ReleaseFast" ;; + zig:fmt) echo "zig fmt ." ;; + zig:lint) echo "zig fmt --check ." ;; + zig:run) grep -qs '"run"' build.zig && echo "zig build run" ;; + + elixir:deps) echo "mix local.hex --force --if-missing && mix local.rebar --force --if-missing && mix deps.get" ;; + elixir:build) echo "mix compile --warnings-as-errors" ;; + elixir:test) echo "mix test" ;; + elixir:bench) ls bench/*.exs >/dev/null 2>&1 && echo "for f in bench/*.exs; do mix run \"\$f\"; done" ;; + elixir:lint) grep -qs ':credo' mix.exs && echo "mix credo --strict" || echo "mix compile --warnings-as-errors" ;; + elixir:fmt) echo "mix format" ;; + elixir:run) echo "mix run --no-halt" ;; + + gleam:deps) echo "gleam deps download" ;; + gleam:build) echo "gleam build" ;; + gleam:test) echo "gleam test" ;; + gleam:fmt) echo "gleam format" ;; + gleam:lint) echo "gleam format --check" ;; + gleam:run) echo "gleam run" ;; + + ocaml:deps) echo "opam install . --deps-only --with-test -y" ;; + ocaml:build) echo "opam exec -- dune build" ;; + ocaml:test) echo "opam exec -- dune test" ;; + ocaml:fmt) echo "opam exec -- dune fmt" ;; + + haskell:deps) echo "cabal update && cabal build all --only-dependencies" ;; + haskell:build) echo "cabal build all" ;; + haskell:test) echo "cabal test all" ;; + haskell:bench) grep -qs '^benchmark' ./*.cabal && echo "cabal bench all" ;; + haskell:run) echo "cabal run" ;; + + bun:deps) [ -f bun.lock ] || [ -f bun.lockb ] && echo "bun install --frozen-lockfile" || echo "bun install" ;; + bun:build) grep -qs '"build"[[:space:]]*:' package.json && echo "bun run build" ;; + bun:test) grep -qs '"test"[[:space:]]*:' package.json && echo "bun run test" || echo "bun test" ;; + bun:bench) grep -qs '"bench"[[:space:]]*:' package.json && echo "bun run bench" ;; + bun:lint) grep -qs '"lint"[[:space:]]*:' package.json && echo "bun run lint" ;; + bun:fmt) grep -qs '"fmt"[[:space:]]*:' package.json && echo "bun run fmt" ;; + bun:run) grep -qs '"start"[[:space:]]*:' package.json && echo "bun run start" ;; + + docs:test) have asciidoctor && echo "for f in \$(git ls-files '*.adoc'); do asciidoctor -o /dev/null --failure-level=WARN \"\$f\" || exit 1; done" ;; + docs:lint) have lychee && echo "lychee --offline --no-progress \$(git ls-files '*.adoc' '*.md')" ;; + esac +} + +# Run a contract verb across every detected language. N/A is reported, not +# faked green: a verb with no command for any language exits 0 but SAYS so. +lang_run() { # $1 verb + local verb=$1 ran=0 rc=0 l cmd override + override=$(deed "$verb" "") + if [ -n "$override" ]; then + info "$verb (from provisioning_praxis.deed): $override" + bash -c "$override"; return $? + fi + for l in "${LANGS[@]}"; do + cmd=$(lang_cmd "$l" "$verb") + [ -z "$cmd" ] && continue + ran=1 + info "$verb [$l]: $cmd" + bash -c "$cmd" || { rc=$?; printf ' %sFAIL%s %s [%s] exited %s\n' "$R" "$Z" "$verb" "$l" "$rc" >&2; } + done + [ $ran -eq 0 ] && info "$verb: N/A for ${LANGS[*]} (archetype: $ARCHETYPE) — nothing to run, and nothing was faked" + return $rc +} + +# --------------------------------------------------------------------------- +# A repo keeps its own checks as root recipes named doctor-local / setup-local / +# heal-local (provision-set renames a pre-existing custom doctor/setup/heal to +# these); the canon verbs run them, so `just doctor`, `./launcher.sh --doctor` and +# CI all see the same result. +has_recipe() { have just && just --summary 2>/dev/null | tr " " "\n" | grep -qx "$1"; } + +version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } + +# Tools that must never appear in a repo's toolchain (estate language policy). +BANNED_TOOLS='python|deno|node|nodejs|npm|yarn|pnpm|typescript|rescript|make|black|ruff|pip|poetry|nix' + +cmd_doctor() { + printf '%s doctor — %s (%s; languages: %s)\n' "$REPO_NAME" "$REPO_SLUG" "$ARCHETYPE" "${LANGS[*]}" + + hdr "Core toolchain" + have git && pass "git $($T git --version 2>/dev/null | awk '{print $3}')" || fail "PV-E01 git not found — install git from your OS package manager" + if have just; then + local jv; jv=$($T just --version 2>/dev/null | awk '{print $2}') + version_ge "$jv" "$MIN_JUST" && pass "just $jv (>= $MIN_JUST)" || fail "PV-E02 just $jv is older than $MIN_JUST — run: mise use just@latest" + else fail "PV-E02 just not found — run: mise use -g just@latest (or see docs/SETUP.adoc)"; fi + if have mise; then + pass "mise $($T mise --version 2>/dev/null | awk '{print $1}')" + if [ -f mise.toml ] || [ -f .mise.toml ]; then + # Only THIS repo's declarations: `mise ls` also lists the user's global config. + local here=${ROOT/#$HOME/\~} missing + missing=$($T mise ls --current --missing 2>/dev/null | grep -F -e "$here/mise.toml" -e "$here/.mise.toml" -e "$ROOT/mise.toml" | awk '{print $1"@"$2}' | tr '\n' ' ') + [ -z "${missing// /}" ] && pass "every mise tool is installed" || fail "PV-E03 mise tools not installed: $missing— run: just setup" + fi + else warn "PV-W01 mise not found — tools must then come from Guix or your OS; install: curl https://mise.run | sh"; fi + if have guix; then pass "guix $($T guix --version 2>/dev/null | head -1 | awk '{print $NF}') (optional reproducible path)" + else info "guix not installed — optional; mise is the default path (docs/SETUP.adoc §Guix)"; fi + + hdr "Language toolchains" + local l t + for l in "${LANGS[@]}"; do + [ "$l" = docs ] && { info "no build language detected — docs/theory archetype"; continue; } + for t in $(lang_tools "$l"); do + if have "$t"; then pass "$l: $t ($(command -v "$t"))" + else fail "PV-E10 $l: '$t' not found — run: just setup (manual: $(lang_remedy "$l"))"; fi + done + done + local extra; extra=$(deed system-deps "") + for t in $extra; do have "$t" && pass "system dep: $t" || fail "PV-E11 system dependency '$t' not found (declared in $DEED) — see docs/SETUP.adoc §Prerequisites"; done + + hdr "Repository provisioning files" + [ -f mise.toml ] && pass "mise.toml" || fail "PV-E20 mise.toml missing — the toolchain is undeclared" + [ -f mise.lock ] && pass "mise.lock (latest → concrete versions)" || warn "PV-W20 mise.lock missing — run: just toolchain-refresh" + [ -f .mise.toml ] && [ -f mise.toml ] && warn "PV-W21 both mise.toml and .mise.toml — mise merges them; keep only mise.toml" + [ -f .tool-versions ] && warn "PV-W22 .tool-versions present — a second toolchain source; fold it into mise.toml" + if [ -f mise.toml ]; then + local bad; bad=$(sed -n '/^\[tools\]/,/^\[/p' mise.toml | grep -oE "^[\"']?($BANNED_TOOLS)[\"']?[[:space:]]*=" | tr -d "\"'= " | tr '\n' ' ') + [ -z "$bad" ] && pass "mise.toml pins no banned tool" || warn "PV-W23 mise.toml pins banned tool(s): $bad(language policy: bun, no python/deno/node/make)" + fi + # hypatia guix_not_stub reads guix.scm AND build/guix.scm; an unfilled __PLACEHOLDER__ is a stub too. + local g gstub="" + for g in guix.scm build/guix.scm manifest.scm; do + [ -f "$g" ] && grep -qE '\{\{|\(inputs \(list\)\)|\(source #f\)|__[A-Z][A-Z_]*__' "$g" && gstub="$gstub$g " + done + if [ -f guix.scm ]; then + if [ -n "$gstub" ]; then warn "PV-W24 template stub in: $gstub— the Guix path does not build this repo (just heal cannot fix this; provision-set realign does)" + else pass "guix.scm (non-stub)"; fi + else warn "PV-W25 guix.scm missing — no reproducible Guix path"; fi + [ -f manifest.scm ] && pass "manifest.scm (guix shell -m manifest.scm)" || warn "PV-W26 manifest.scm missing — 'just dev-shell' falls back to mise" + [ -x launcher.sh ] && pass "launcher.sh (executable)" || { [ -f launcher.sh ] && fail "PV-E27 launcher.sh not executable — run: just heal" || warn "PV-W27 launcher.sh missing"; } + local d + for d in docs/SETUP.adoc docs/AI_INSTALLATION_GUIDE.adoc llm-warmup-user.adoc llm-warmup-dev.adoc llm-warmup-maintainer.adoc; do + if [ ! -f "$d" ]; then warn "PV-W28 $d missing" + elif grep -qE "__[A-Z][A-Z_]*__" "$d"; then warn "PV-W29 $d still has unfilled template slots (__SPEC_…__) — replace each with the facts for this repo" + else pass "$d"; fi + done + + hdr "Estate policy drift" + local f any=0 + for f in deno.json deno.jsonc deno.lock import_map.json; do + [ -f "$f" ] && { warn "PV-W30 $f — Deno leftover; the estate runtime is bun (migration issue tracks it)"; any=1; } + done + for f in flake.nix flake.lock shell.nix default.nix; do [ -f "$f" ] && { warn "PV-W31 $f — Nix is not an estate toolchain; Guix is"; any=1; }; done + for f in Makefile GNUmakefile makefile; do [ -f "$f" ] && { warn "PV-W32 $f — Justfile is the only task runner"; any=1; }; done + for f in requirements.txt pyproject.toml setup.py Pipfile; do [ -f "$f" ] && { warn "PV-W33 $f — Python is not an estate language"; any=1; }; done + for f in package-lock.json yarn.lock pnpm-lock.yaml tsconfig.json; do [ -f "$f" ] && { warn "PV-W34 $f — npm/yarn/pnpm/TypeScript leftover; bun only"; any=1; }; done + [ $any -eq 0 ] && pass "no deno / nix / make / python / npm leftovers" + + local hook=build/just/doctor-local.sh + if [ -f "$hook" ]; then hdr "Repo-specific checks ($hook)"; # shellcheck source=/dev/null + . "$hook"; fi + if has_recipe doctor-local; then + hdr "Repo-specific checks (just doctor-local)" + just doctor-local && pass "doctor-local" || fail "PV-E50 the repo-specific doctor-local recipe failed (output above)" + fi + + printf '\n%s: %s%d PASS%s, %s%d WARN%s, %s%d FAIL%s\n' "$REPO_NAME" "$G" "$PASS" "$Z" "$Y" "$WARN" "$Z" "$R" "$FAIL" "$Z" + if [ "$FAIL" -gt 0 ]; then + echo "Next: 'just heal' fixes what is safe to fix automatically; each FAIL code is explained in docs/SETUP.adoc §Troubleshooting." + return 1 + fi + return 0 +} + +cmd_setup() { + printf '%s setup — installing everything this repository needs\n' "$REPO_NAME" + local rc=0 + if have mise; then + hdr "mise toolchain" + $T mise trust -q . 2>/dev/null || true + mise install || rc=1 + else + warn "mise not found. Install it (curl https://mise.run | sh), or enter 'guix shell -m manifest.scm', then re-run: just setup" + fi + local l + for l in "${LANGS[@]}"; do + case "$l" in + idris2) have pack || warn "pack not found — Idris2 comes from pack, not mise: $(lang_remedy idris2)" ;; + haskell) have ghcup || have ghc || warn "ghc not found — $(lang_remedy haskell)" ;; + ocaml) have opam && { opam switch show >/dev/null 2>&1 || opam init -y --bare; } ;; + esac + done + hdr "Project dependencies" + if have mise; then eval "$(mise env -s bash 2>/dev/null)" || true; fi + lang_run deps || rc=1 + local hook=build/just/setup-local.sh + [ -f "$hook" ] && { hdr "Repo-specific setup ($hook)"; bash "$hook" || rc=1; } + has_recipe setup-local && { hdr "Repo-specific setup (just setup-local)"; just setup-local || rc=1; } + [ -f launcher.sh ] && chmod +x launcher.sh + hdr "Verification" + cmd_doctor || rc=1 + [ $rc -eq 0 ] && printf '\n%sReady.%s Try: just --list\n' "$G" "$Z" || printf '\n%sSetup incomplete%s — read the FAIL lines above; docs/SETUP.adoc has the manual route.\n' "$R" "$Z" + return $rc +} + +cmd_heal() { + printf '%s heal — applying safe, reversible fixes, then re-checking\n' "$REPO_NAME" + hdr "Fixes" + [ -f launcher.sh ] && [ ! -x launcher.sh ] && chmod +x launcher.sh && info "made launcher.sh executable" + for f in build/just/*.sh scripts/*.sh; do [ -f "$f" ] && [ ! -x "$f" ] && chmod +x "$f" && info "made $f executable"; done + if have mise; then + $T mise trust -q . 2>/dev/null && info "mise config trusted" + mise install && info "mise tools installed" + [ -f mise.lock ] || { $T mise lock 2>/dev/null && info "mise.lock generated"; } + fi + if printf '%s\n' "${LANGS[@]}" | grep -qx elixir && have mix; then mix deps.get >/dev/null 2>&1 && info "mix deps fetched"; fi + if printf '%s\n' "${LANGS[@]}" | grep -qx bun && have bun; then bun install >/dev/null 2>&1 && info "bun deps installed"; fi + if printf '%s\n' "${LANGS[@]}" | grep -qx julia && have julia; then julia --project=. -e 'using Pkg; Pkg.instantiate()' >/dev/null 2>&1 && info "julia deps instantiated"; fi + local hook=build/just/heal-local.sh + [ -f "$hook" ] && { info "running $hook"; bash "$hook" || true; } + has_recipe heal-local && { info "running just heal-local"; just heal-local || true; } + echo "Not touched (never automatic): source files, git history, Deno/Nix/Make leftovers — those are migrations, tracked as issues." + hdr "Re-check" + cmd_doctor +} + +cmd_dev_shell() { + if have guix && [ -f manifest.scm ] && ! grep -q '{{' manifest.scm; then + info "entering: guix shell -m manifest.scm (exit to leave)" + exec guix shell -m manifest.scm + elif have mise; then + info "guix/manifest.scm unavailable — entering the mise environment instead (exit to leave)" + exec mise exec -- "${SHELL:-bash}" + else + fail "PV-E40 neither guix nor mise is installed — see docs/SETUP.adoc"; return 1 + fi +} + +cmd_toolchain_refresh() { + hdr "mise: bump 'latest' resolutions and re-lock" + have mise || { fail "mise not found"; return 1; } + mise up --bump || return 1 + $T mise lock 2>/dev/null || mise lock || return 1 + if [ -f channels.scm ]; then + hdr "guix: channel pin" + if have guix; then + guix pull --channels=channels.scm --dry-run >/dev/null 2>&1 || true + guix describe --format=channels > channels.scm.new 2>/dev/null && mv channels.scm.new channels.scm && info "channels.scm re-pinned to the current guix commit" + else info "guix not installed — channels.scm left as-is (CI re-pins it)"; fi + fi + git --no-pager diff --stat -- mise.toml mise.lock channels.scm 2>/dev/null + echo "Commit the diff above (signed) as: chore(toolchain): weekly refresh" +} + +just_say_it() { + local line; line=$(deed ai-say-it "") + [ -z "$line" ] && line="Set up $REPO_NAME from https://github.com/$REPO_SLUG — follow docs/AI_INSTALLATION_GUIDE.adoc in that repo." + printf '%s' "$line" +} +clip() { # best-effort; silent when no clipboard exists (CI, SSH) + # wl-copy and xclip fork a daemon that inherits stdout; left attached, it + # holds any pipeline open forever (observed 2026-09-30). Detach and bound it. + local c=() + if have clip.exe; then c=(clip.exe) # WSL: the Windows clipboard + elif have pbcopy; then c=(pbcopy) + elif [ -n "${WAYLAND_DISPLAY:-}" ] && have wl-copy; then c=(wl-copy) + elif [ -n "${DISPLAY:-}" ] && have xclip; then c=(xclip -selection clipboard) + else cat >/dev/null; return 1; fi + timeout 3 "${c[@]}" >/dev/null 2>&1 +} +cmd_ai_setup() { + cat <" >&2; return 2 ;; esac + for f in "llm-warmup-$who.adoc" "docs/llm-warmup-$who.adoc"; do + if [ -f "$f" ]; then + cat "$f"; clip < "$f" && echo "--- (copied $f to your clipboard — paste it as your first message to any AI)" >&2 || true + return 0 + fi + done + fail "llm-warmup-$who.adoc not found"; return 1 +} + +cmd_eval() { + local logf rc=0 s e v r o st + logf=".eval/$(date -u +%Y%m%dT%H%M%SZ).txt" + mkdir -p .eval + { echo "# $REPO_NAME evaluation — $(date -u +%FT%TZ) — $(git rev-parse --short HEAD 2>/dev/null || echo no-git)" + echo "# languages: ${LANGS[*]} archetype: $ARCHETYPE"; } > "$logf" + for v in test bench; do + # The root recipe if the repo defines one (its own override), else the module default. + if just --summary 2>/dev/null | tr " " "\n" | grep -qx "$v"; then r=$v; else r="provision::$v"; fi + s=$(date +%s); o=$(mktemp) + if just "$r" >"$o" 2>&1; then st=PASS; else st=FAIL; rc=1; fi + e=$(( $(date +%s) - s )); cat "$o" >>"$logf" + # A skip is not a pass: lang_run exits 0 on N/A (users see success), eval says N/A. + [ "$st" = PASS ] && grep -q "nothing was faked" "$o" && st=N/A + rm -f "$o"; echo "$v: $st (${e}s)" | tee -a "$logf" + done + echo "full log: $logf" + return $rc +} + +cmd_config_show() { + echo "repo: $REPO_SLUG" + echo "archetype: $ARCHETYPE" + echo "languages: ${LANGS[*]}" + echo "descriptor: $DEED $([ -f "$DEED" ] && echo '(present)' || echo '(absent — defaults in use)')" + echo "config: $(deed config "none declared")" + echo "run: $(deed run "$(for l in "${LANGS[@]}"; do lang_cmd "$l" run; done | head -1)")" + echo "ports: $(deed ports "none")" + echo "mise: $([ -f mise.toml ] && echo mise.toml) $([ -f mise.lock ] && echo mise.lock)" + echo "guix: $(for f in guix.scm manifest.scm channels.scm; do [ -f "$f" ] && printf "%s " "$f"; done)" + echo "lib: provision-lib $PROVISION_LIB_VERSION" +} + +cmd_opsm() { + cat <} + if have agrep; then git ls-files -z | xargs -0 agrep -n -1 -- "$p" 2>/dev/null + else git grep -n -i -- "$p"; fi +} + +case "${1:-}" in + guix-specs) guix_specs ;; + mise-tools) mise_tools ;; + langs) printf '%s\n' "${LANGS[@]}" ;; + doctor) cmd_doctor ;; + setup) cmd_setup ;; + heal) cmd_heal ;; + dev-shell) cmd_dev_shell ;; + toolchain-refresh) cmd_toolchain_refresh ;; + ai-setup) cmd_ai_setup ;; + ai-warmup) shift; cmd_ai_warmup "${1:-user}" ;; + eval) cmd_eval ;; + config-show) cmd_config_show ;; + opsm) cmd_opsm ;; + search) shift; cmd_search "${1:-}" ;; + lang-run) shift; lang_run "${1:?verb}" ;; + version) echo "provision-lib $PROVISION_LIB_VERSION" ;; + *) sed -n '2,20p' "$0"; exit 2 ;; +esac diff --git a/3-practice/provisioning/templates/build/just/provision-modes.sh b/3-practice/provisioning/templates/build/just/provision-modes.sh new file mode 100755 index 000000000..8e2a5e83c --- /dev/null +++ b/3-practice/provisioning/templates/build/just/provision-modes.sh @@ -0,0 +1,138 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# shellcheck shell=bash +# +# provision-modes.sh — the launcher-standard 0.5.0 provisioning mode family, +# as a SOURCEABLE block shared by every launcher in the estate. +# +# New launchers (library/tool/theory/docs): templates/launcher.sh.tmpl sources +# this and calls hp_launcher_main. +# Existing app launchers: add ONE line before their mode switch — +# +# . "$REPO_DIR/build/just/provision-modes.sh" && hp_provision_dispatch "$@" && exit $? +# +# hp_provision_dispatch returns 99 when the mode is not a provisioning +# mode, so the app's own switch then handles it. +# +# Canon: hyperpolymath/standards launcher/launcher-standard_praxis.deed +# (archetypes, provisioning-modes) and +# 3-practice/provisioning/PROVISIONING-STANDARD.adoc + +HP_PROVISION_MODES_VERSION="0.5.0" + +hp__deed() { # $1 key, $2 default — flat (key "value") read from provisioning_praxis.deed + local f="$REPO_DIR/.machine_readable/descriptiles/provisioning_praxis.deed" v="" + [ -f "$f" ] && v=$(grep -oE "[(:]$1[[:space:]]+\"[^\"]*\"" "$f" | head -1 | sed -E 's/^[^"]*"//; s/"$//') + printf '%s' "${v:-$2}" +} + +hp_archetype() { hp__deed archetype "library"; } +hp_app_name() { hp__deed name "$(basename "$REPO_DIR")"; } + +hp_platform() { + case "$(uname -s)" in + Linux*) echo linux ;; + Darwin*) echo macos ;; + CYGWIN*|MINGW*|MSYS*|Windows_NT) echo windows ;; + *) echo unknown ;; + esac +} + +# Make sure `just` is runnable: PATH, then mise, then say exactly what to do. +hp_ensure_just() { + command -v just >/dev/null 2>&1 && return 0 + if command -v mise >/dev/null 2>&1; then + echo "just is not installed — installing it with mise (mise use -g just@latest)..." >&2 + mise use -g just@latest >&2 && eval "$(mise env -s bash 2>/dev/null)" && command -v just >/dev/null 2>&1 && return 0 + mise exec just@latest -- true >/dev/null 2>&1 && { just() { mise exec just@latest -- just "$@"; }; return 0; } + fi + cat >&2 <<'EOF' +Neither `just` nor `mise` is installed. Install mise (it then installs everything else): + + curl https://mise.run | sh # Linux / macOS / WSL + winget install jdx.mise # Windows + +then re-run this command. Manual route without mise: docs/SETUP.adoc +EOF + return 1 +} + +hp_just() { hp_ensure_just || return 1; (cd "$REPO_DIR" && just "$@"); } +# The provisioning modes call the engine directly, not a `just` recipe: a repo may +# define its own root `doctor`/`setup`/`heal`, and the launcher must still run the +# canon (which then runs that repo's *-local recipes). Only needs bash. +hp_lib() { (cd "$REPO_DIR" && bash build/just/provision-lib.sh "$@"); } + +# Returns the mode's exit code, or 99 when "$1" is not a provisioning mode. +hp_provision_dispatch() { + case "${1:-}" in + --setup) hp_lib setup ;; + --doctor) hp_lib doctor ;; + --heal) hp_lib heal ;; + --ai-setup) hp_lib ai-setup ;; + *) return 99 ;; + esac +} + +hp_version_line() { + local sha ver + sha=$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown) + ver=$(hp__deed version "") + [ -z "$ver" ] && ver=$(git -C "$REPO_DIR" describe --tags --abbrev=0 2>/dev/null || echo 0.0.0) + printf '%s-launcher %s (%s) [%s-%s]\n' "$(hp_app_name)" "${ver#v}" "$sha" "$(hp_platform)" "$(uname -m)" +} + +hp_help() { + local name arch; name=$(hp_app_name); arch=$(hp_archetype) + cat < + +Provisioning (every repository): + --setup Install everything this repository needs, then run --doctor + --doctor Check the environment; PASS/WARN/FAIL with fix hints (exit 1 on FAIL) + --heal Apply safe fixes automatically, then re-run --doctor + --ai-setup Print the one line to give any AI assistant to set this up for you + +Meta: + --help This text + --version Machine-readable version line + +EOF + if [ "$arch" = app ]; then + echo "Runtime: --start --stop --status --auto (default) --integ --disinteg" + else + echo "Runtime modes (--start --stop --status --auto --integ --disinteg) do not apply" + echo "to a $arch repository; they are accepted and explain themselves." + fi + cat <&2 + return 1 + fi + echo "$(hp_app_name) is a $arch: there is nothing to ${mode#--}. Try --setup, --doctor or --help." + return 0 ;; + *) + echo "Unknown mode: $mode" >&2; hp_help >&2; return 2 ;; + esac +} diff --git a/3-practice/provisioning/templates/build/just/provision.just b/3-practice/provisioning/templates/build/just/provision.just new file mode 100644 index 000000000..c7d65f9bb --- /dev/null +++ b/3-practice/provisioning/templates/build/just/provision.just @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# provision.just — the estate provisioning contract, as a just MODULE. +# +# Canon: hyperpolymath/standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc +# Loaded from the root Justfile with: +# +# mod provision 'build/just/provision.just' +# +# A module (not `import`) so these recipes live under `provision::` and never +# collide with a repo's own `doctor`, `heal` or `test`. The root Justfile then +# exposes each contract name ONCE — either its own recipe, or a one-line +# delegation such as `doctor: provision::doctor` that `provision-set mint` +# adds only when the root has no recipe of that name. +# +# requires: just >= 1.42.0 (root recipes depend on module recipes; measured 2026-09-30: 1.41 rejects `x: m::x`, 1.42 accepts) + +root := justfile_directory() +lib := "PROVISION_ROOT=" + quote(root) + " bash " + quote(source_directory() / "provision-lib.sh") + +# List the provisioning recipes +default: + @just --list provision + +# Install every tool and dependency this repo needs, then run doctor +setup: + @{{lib}} setup + +# Diagnose the environment: PASS/WARN/FAIL with codes; exits 1 on any FAIL +doctor: + @{{lib}} doctor + +# Apply safe, reversible fixes, then re-run doctor +heal: + @{{lib}} heal + +# Enter a shell with every tool on PATH (guix shell -m manifest.scm, else mise) +dev-shell: + @{{lib}} dev-shell + +# Weekly: bump mise 'latest' resolutions, re-lock, re-pin guix channels +toolchain-refresh: + @{{lib}} toolchain-refresh + +# Print (and copy) the one line to give any AI to install this repo +ai-setup: + @{{lib}} ai-setup + +# Print (and copy) the warm-up for an AI session: user, dev or maintainer +ai-warmup who="user": + @{{lib}} ai-warmup {{who}} + +# Run test + bench, time both, and write .eval/.txt +eval: + @{{lib}} eval + +# Show where this repo's configuration and toolchain are declared +config-show: + @{{lib}} config-show + +# How to install this repo with OPSM (odds-and-sods package manager) +opsm: + @{{lib}} opsm + +# Print the languages detected from marker files +langs: + @{{lib}} langs + +# Approximate search across tracked files (agrep, else git grep) +search pattern: + @{{lib}} search {{quote(pattern)}} + +# Per-language defaults for the contract verbs (used when the root Justfile +# has no recipe of its own) +build: + @{{lib}} lang-run build + +test: + @{{lib}} lang-run test + +bench: + @{{lib}} lang-run bench + +lint: + @{{lib}} lang-run lint + +fmt: + @{{lib}} lang-run fmt + +run: + @{{lib}} lang-run run + +deps: + @{{lib}} lang-run deps diff --git a/3-practice/provisioning/templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl b/3-practice/provisioning/templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl new file mode 100644 index 000000000..ca41c5732 --- /dev/null +++ b/3-practice/provisioning/templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: __DOC_LICENSE__ +// SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ += AI Installation Guide — __APP_NAME__ +:toc: + +[IMPORTANT] +==== +*This file is written for an AI assistant.* A person asked you to set up +__APP_NAME__ (https://github.com/__REPO_SLUG__). Follow these steps in order. Run +each command yourself if you can run commands; otherwise give the person one +command at a time and wait for its output. Never skip the privacy notice (step 2) +or the verification (step 6). If a step fails, go to <>; do not +improvise a different installer. +==== + +== What this is + +__APP_NAME__ is a __ARCHETYPE__ written in __LANGS__. +__SPEC_WHAT_IT_IS__ + +== 1. Ask the person + +Ask only these, then proceed: + +. *Which operating system?* Linux (which distribution), macOS, or Windows. On +Windows, the steps run inside WSL2; if they have no WSL2, have them run +`wsl --install` in an administrator PowerShell and reboot first. +. *Where should it live?* Default: `~/src/__APP_NAME__`. +__SPEC_QUESTIONS__ + +== 2. Tell the person what will happen (privacy notice) + +Show them this, and continue only after they agree: + +____ +Setting up __APP_NAME__ will: + +* clone https://github.com/__REPO_SLUG__ into the folder you chose; +* install developer tools with mise into `~/.local/share/mise` (per-user, no root), + from their official releases: __MISE_TOOLS__; +* fetch this project's own dependencies from their public registries. + +__SPEC_PRIVACY__ + +It will not: change system settings, need administrator rights (except the OS +packages in step 3, which you approve individually), send data anywhere, or +install anything outside the folders above. +____ + +== 3. Prerequisites + +Check each; install only what is missing. + +[source,bash] +---- +git --version # any version +mise --version # any version +---- + +* Missing git: Fedora `sudo dnf install git` · Debian/Ubuntu `sudo apt install git` · macOS `xcode-select --install`. +* Missing mise: `curl https://mise.run | sh`, then run the activation line it + prints (e.g. `echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc`) + and open a new shell. +* A C toolchain, if `cc --version` fails: Fedora `sudo dnf install gcc` · Debian/Ubuntu `sudo apt install build-essential`. +__SYSTEM_DEPS_AI__ + +== 4. Fetch and set up + +[source,bash] +---- +git clone https://github.com/__REPO_SLUG__.git ~/src/__APP_NAME__ +cd ~/src/__APP_NAME__ +./launcher.sh --setup +---- + +`./launcher.sh --setup` installs `just` if needed, runs `mise install`, fetches +this project's dependencies, and ends with `just doctor`. It may take several +minutes the first time; that is normal. + +== 5. If you cannot run the launcher + +Run the same steps directly: + +[source,bash] +---- +mise trust && mise install +just setup +---- + +The full manual route, with every step explained, is `docs/SETUP.adoc`. + +== 6. Verify + +[source,bash] +---- +just doctor +---- + +Success is: exit status 0 and the last line reads `__APP_NAME__: N PASS, N WARN, 0 FAIL`. +WARN lines are advice; mention them to the person but they do not block use. +Then confirm it works: + +[source,bash] +---- +__SPEC_VERIFY__ +---- + +== 7. Tell the person how to use it + +__SPEC_USAGE__ + +Every task is a `just` recipe; `just` lists them. `./launcher.sh --help` explains +the launcher. + +== Uninstall + +[source,bash] +---- +rm -rf ~/src/__APP_NAME__ # the checkout (and everything it built) +---- + +Tools mise installed are shared with other projects; remove one only if nothing +else uses it: `mise uninstall @` (`mise ls` lists them). +__SPEC_UNINSTALL__ + +[[if-a-step-fails]] +== If a step fails + +. Run `just doctor` and read the code on each FAIL line (PV-E…). +. Look it up in `docs/SETUP.adoc` §Troubleshooting and apply the fix. `just heal` + applies every automatic fix. +. Re-run `just doctor`. +. If it still fails, save `just doctor > doctor.txt 2>&1` and help the person open + an issue at https://github.com/__REPO_SLUG__/issues with that file. diff --git a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl new file mode 100644 index 000000000..60c3f608a --- /dev/null +++ b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: __DOC_LICENSE__ +// SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ += Setting up __APP_NAME__ by hand +:toc: +:toclevels: 2 + +This is the complete manual route: every step, in order, with nothing assumed. +You do not need it if you use `./launcher.sh --setup` (or ask an AI, see +link:AI_INSTALLATION_GUIDE.adoc[AI_INSTALLATION_GUIDE.adoc]), but those do +exactly what is written here, so this page is also what to read when they stop. + +[horizontal] +Repository:: https://github.com/__REPO_SLUG__ +Kind:: __ARCHETYPE__ (__LANGS__) +Toolchain declared in:: `mise.toml` (versions `latest`, made concrete in `mise.lock`), and +`manifest.scm` / `guix.scm` / `channels.scm` for Guix +Every command below is also a `just` recipe:: run `just` to list them + +[[prerequisites]] +== 1. Prerequisites + +You need three things from your operating system. Everything else is installed +for you in step 3. + +[cols="1,2,3"] +|=== +|Tool |Why |Install + +|git +|to fetch the repository +|Fedora `sudo dnf install git` · Debian/Ubuntu `sudo apt install git` · macOS `xcode-select --install` · Windows `winget install Git.Git` + +|a C toolchain +|some language toolchains link native code +|Fedora `sudo dnf install gcc` · Debian/Ubuntu `sudo apt install build-essential` · macOS (included with the Xcode tools above) · Windows: use WSL2 + +|mise +|installs every other tool at the version this repo declares +|Linux/macOS/WSL `curl https://mise.run \| sh` · Windows `winget install jdx.mise` — then follow the one line it prints to activate it in your shell +|=== + +NOTE: On Windows, use WSL2 (`wsl --install`) and follow the Linux steps inside +it. The launcher detects the platform and says so in `./launcher.sh --version`. + +__SYSTEM_DEPS_SECTION__ + +== 2. Get the code + +[source,bash] +---- +git clone https://github.com/__REPO_SLUG__.git +cd __APP_NAME__ +---- + +Or with OPSM, the odds-and-sods package manager (see <>): +`opsm install https://github.com/__REPO_SLUG__.git --registry git` + +== 3. Install the toolchain + +[source,bash] +---- +mise trust # allow this repository's mise.toml +mise install # installs everything mise.toml lists, at mise.lock's versions +---- + +That installs: + +[cols="1,2,3"] +|=== +|Language |Tools |If mise cannot supply it + +__TOOL_TABLE__ +|=== + +`just` itself is installed by `mise install`. Without mise, install just from your +OS instead (`dnf install just`, `brew install just`, `cargo install just`); this +repository needs just 1.42 or newer. + +== 4. Set it up + +[source,bash] +---- +just setup +---- + +`just setup` repeats step 3 (harmless), fetches this repository's own +dependencies (__LANGS__), and finishes by running `just doctor`. + +== 5. Check it + +[source,bash] +---- +just doctor # PASS / WARN / FAIL for every requirement, with the fix for each +---- + +`just doctor` exits 0 when nothing FAILs. A WARN is advice, not a failure. +Every code it prints is explained in <>. + +== 6. Use it + +[source,bash] +---- +just build # build +just test # run the tests +just bench # run the benchmarks (says N/A when there are none) +just eval # tests + benchmarks with timings, saved under .eval/ +just run # run it (says N/A for a library) +just config-show # where the toolchain and config are declared +---- + +Anything that does not apply to a __ARCHETYPE__ says so and exits 0; nothing is faked. + +== 7. Keep it current + +[source,bash] +---- +just toolchain-refresh # mise up --bump, re-lock mise.lock, re-pin channels.scm +just heal # re-apply safe fixes if something drifted +---- + +[[guix]] +== The Guix route (alternative to steps 3–4) + +With GNU Guix installed you can skip mise for everything Guix packages: + +[source,bash] +---- +guix shell -m manifest.scm # the development shell +guix time-machine -C channels.scm -- shell -m manifest.scm # the exact, pinned Guix +guix build -f guix.scm # the package +---- + +Guix does not package __GUIX_GAPS__; the Guix shell includes mise, so inside it run +`mise install` for those. `just dev-shell` picks Guix when it is present and mise +when it is not. + +[[opsm]] +== OPSM (odds-and-sods package manager) + +[source,bash] +---- +opsm install https://github.com/__REPO_SLUG__.git --registry git +---- + +__APP_NAME__ has no registry package, so OPSM fetches it through its git +adapter; then provision it with `./launcher.sh --setup`. To get OPSM itself +(Erlang/OTP 26+ and Elixir 1.16+ required): + +[source,bash] +---- +git clone https://github.com/hyperpolymath/odds-and-sods-package-manager.git +cd odds-and-sods-package-manager/opsm_ex +mix deps.get && mix escript.build +install -m 0755 opsm ~/.local/bin/opsm +opsm --version +---- + +`just opsm` prints the same instructions. + +[[troubleshooting]] +== Troubleshooting + +Run `just doctor`, find the code it printed, apply the fix. `just heal` applies +every fix marked *auto* for you. + +[cols="1,3,3,1"] +|=== +|Code |Meaning |Fix |Auto + +|PV-E01 |git is not installed |Install git (step 1) |no +|PV-E02 |just is missing or older than 1.42 |`mise use -g just@latest` |no +|PV-E03 |a tool in mise.toml is not installed |`just setup` (or `mise install`) |yes +|PV-E10 |a language toolchain binary is missing |`just setup`; the doctor line names the manual route |yes, via mise +|PV-E11 |a system dependency from the provisioning deed is missing |install it from your OS (see <>) |no +|PV-E20 |mise.toml is missing |restore it: `git checkout -- mise.toml` |no +|PV-E27 |launcher.sh is not executable |`chmod +x launcher.sh` |yes +|PV-E40 |neither Guix nor mise is installed, so there is no dev shell |install mise (step 1) |no +|PV-E50 |this repository's own `doctor-local` checks failed |read the output above the summary line; the recipe is in the Justfile |no +|PV-W01 |mise is not installed |install mise (step 1) |no +|PV-W20 |mise.lock is missing, so `latest` is not concrete |`just toolchain-refresh` |yes +|PV-W21 |both mise.toml and .mise.toml exist |merge into mise.toml, delete .mise.toml |no +|PV-W22 |a .tool-versions file competes with mise.toml |fold it into mise.toml |no +|PV-W23 |mise.toml pins a tool the estate does not use |remove that line |no +|PV-W24 |a Guix file is an unfilled template |regenerate it (maintainers: `provision-set realign`) |no +|PV-W25 |guix.scm is missing |as PV-W24 |no +|PV-W26 |manifest.scm is missing |as PV-W24; mise still works |no +|PV-W27 |launcher.sh is missing |as PV-W24 |no +|PV-W28 |a setup document is missing |as PV-W24 |no +|PV-W29 |a setup document still holds unfilled template slots |replace each `__SPEC_…__` slot with the facts for this repository |no +|PV-W30 |Deno files remain |the estate runtime is bun; a migration issue tracks it |no +|PV-W31 |Nix files remain |use Guix |no +|PV-W32 |a Makefile remains |use the Justfile |no +|PV-W33 |Python files remain |not an estate language |no +|PV-W34 |npm/yarn/pnpm/TypeScript files remain |use bun |no +|=== + +Still stuck? `just doctor > doctor.txt 2>&1` and open an issue at +https://github.com/__REPO_SLUG__/issues with that file attached. diff --git a/3-practice/provisioning/templates/guix/channels.scm b/3-practice/provisioning/templates/guix/channels.scm new file mode 100644 index 000000000..54ac61a26 --- /dev/null +++ b/3-practice/provisioning/templates/guix/channels.scm @@ -0,0 +1,18 @@ +;; SPDX-License-Identifier: MPL-2.0 +;; channels.scm — the Guix revision this repository's guix.scm and manifest.scm +;; were verified against. Reproduce that exact Guix with: +;; +;; guix time-machine -C channels.scm -- shell -m manifest.scm +;; +;; Refresh it (after re-verifying) with: just toolchain-refresh +;; Canon pin, verified 2026-09-30 (docker.io/metacall/guix, guix describe). +(list (channel + (name 'guix) + (url "https://codeberg.org/guix/guix.git") + (branch "master") + (commit "ae77aeb9543de2661d739104bc4d3803d8c6f38b") + (introduction + (make-channel-introduction + "9edb3f66fd807b096b48283debdcddccfea34bad" + (openpgp-fingerprint + "BBB0 2DDF 2CEA F6A8 0D1D E643 A2A0 6DF2 A33A 54FA"))))) diff --git a/3-practice/provisioning/templates/guix/guix.scm.cargo.tmpl b/3-practice/provisioning/templates/guix/guix.scm.cargo.tmpl new file mode 100644 index 000000000..94ef2ef79 --- /dev/null +++ b/3-practice/provisioning/templates/guix/guix.scm.cargo.tmpl @@ -0,0 +1,37 @@ +;; SPDX-License-Identifier: __LICENSE__ +;; SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ +;; +;; guix.scm — builds __APP_NAME__ hermetically with Guix (offline, from Cargo.lock). +;; +;; guix build -f guix.scm # the package +;; guix shell -D -f guix.scm # its build environment +;; +;; Crate inputs live in build/guix/crates.scm, generated from Cargo.lock by +;; guix import crate --lockfile=Cargo.lock __APP_NAME__ +;; `just toolchain-refresh` regenerates it; never hand-edit it. +(use-modules (guix packages) (guix gexp) + (guix build-system cargo) + ((guix licenses) #:prefix license:)) + +(define %source-dir (dirname (current-filename))) +(load (string-append %source-dir "/build/guix/crates.scm")) ; defines %crate-inputs + +;; Build products and caches never enter the source (git-predicate is not used: +;; it breaks on tarball checkouts and on bind-mounted trees). +(define %ignored + '(".git" "target" ".eval" "node_modules" "_build" "deps" "zig-out" ".zig-cache" "dist-newstyle")) + +(package + (name "__APP_NAME__") + (version "__APP_VERSION__") + (source (local-file %source-dir "__APP_NAME__-checkout" + #:recursive? #t + #:select? (lambda (file stat) + (not (member (basename file) %ignored))))) + (build-system cargo-build-system) + (arguments (list #:install-source? #f)) + (inputs %crate-inputs) + (home-page "https://github.com/__REPO_SLUG__") + (synopsis "__SYNOPSIS__") + (description "__DESCRIPTION__") + (license __GUIX_LICENSE__)) diff --git a/3-practice/provisioning/templates/guix/guix.scm.source.tmpl b/3-practice/provisioning/templates/guix/guix.scm.source.tmpl new file mode 100644 index 000000000..e6e41f856 --- /dev/null +++ b/3-practice/provisioning/templates/guix/guix.scm.source.tmpl @@ -0,0 +1,40 @@ +;; SPDX-License-Identifier: __LICENSE__ +;; SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ +;; +;; guix.scm — __APP_NAME__ as a Guix package. +;; +;; guix build -f guix.scm # installs the source tree to share/__APP_NAME__ +;; guix shell -D -f guix.scm # the full development toolchain +;; +;; This is a SOURCE package, and says so: a hermetic compiled build needs this +;; repository's __LANGS__ dependencies packaged in Guix, which they are not +;; (Guix builds offline). The toolchain below is real — `guix shell -D -f guix.scm` +;; then `just setup` gives a working environment. See docs/SETUP.adoc §Guix. +(use-modules (guix packages) (guix gexp) + (guix build-system copy) + (gnu packages) + ((guix licenses) #:prefix license:)) + +(define %source-dir (dirname (current-filename))) +;; A spec may name an output ("rust:cargo"); plain specification->package cannot. +(define (spec->input spec) + (call-with-values (lambda () (specification->package+output spec)) + (lambda (pkg out) (if (string=? out "out") pkg (list pkg out))))) + +(define %ignored + '(".git" "target" ".eval" "node_modules" "_build" "deps" "zig-out" ".zig-cache" "dist-newstyle")) + +(package + (name "__APP_NAME__") + (version "__APP_VERSION__") + (source (local-file %source-dir "__APP_NAME__-checkout" + #:recursive? #t + #:select? (lambda (file stat) + (not (member (basename file) %ignored))))) + (build-system copy-build-system) + (arguments (list #:install-plan #~'(("." "share/__APP_NAME__/")))) + (native-inputs (map spec->input (list __GUIX_PKG_SPECS__))) + (home-page "https://github.com/__REPO_SLUG__") + (synopsis "__SYNOPSIS__") + (description "__DESCRIPTION__") + (license __GUIX_LICENSE__)) diff --git a/3-practice/provisioning/templates/guix/manifest.scm.tmpl b/3-practice/provisioning/templates/guix/manifest.scm.tmpl new file mode 100644 index 000000000..0795c6a32 --- /dev/null +++ b/3-practice/provisioning/templates/guix/manifest.scm.tmpl @@ -0,0 +1,14 @@ +;; SPDX-License-Identifier: __LICENSE__ +;; SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ +;; +;; manifest.scm — the Guix development shell for __APP_NAME__. +;; +;; guix shell -m manifest.scm # or: just dev-shell +;; guix time-machine -C channels.scm -- shell -m manifest.scm # the pinned Guix +;; +;; Minted by provision-set from the languages detected here (__LANGS__). +;; Tools Guix does not package — __GUIX_GAPS__ — come from mise, which this +;; shell provides: guix shell -m manifest.scm -- mise install +;; Canon: hyperpolymath/standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc +(specifications->manifest + (list __GUIX_SPECS__)) diff --git a/3-practice/provisioning/templates/launcher.sh.tmpl b/3-practice/provisioning/templates/launcher.sh.tmpl new file mode 100755 index 000000000..1777fde67 --- /dev/null +++ b/3-practice/provisioning/templates/launcher.sh.tmpl @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: __LICENSE__ +# SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ +# +# @launcher-deed begin +# ;; SPDX-License-Identifier: __LICENSE__ +# (praxis-deed +# :schema-version "1.0.0" +# :canonical-name "__APP_NAME__-launcher" +# :beholding-chora #u5"estate/chora" +# (artefact :type "launcher" :version "__APP_VERSION__" +# :generator "provision-set") +# (app :name "__APP_NAME__" :display "__APP_NAME__" +# :url "https://github.com/__REPO_SLUG__" :archetype "__ARCHETYPE__") +# (compliance :standard-version "0.5.0" +# :standards ("launcher-standard.adoc" +# "PROVISIONING-STANDARD.adoc")) +# (modes :accepted ("--setup" "--doctor" "--heal" "--ai-setup" "--help" "--version" +# "--start" "--stop" "--status" "--auto" "--integ" "--disinteg")) +# (platforms :supported ("linux" "macos" "windows")) +# (lifecycle-phases :covered ("provision" "diagnose" "heal") +# :deferred ("install" "run"))) +# @launcher-deed end +# +# The launcher for a library / tool / theory / docs repository +# (launcher-standard 0.5.0, archetype profile). Every mode is implemented in +# build/just/provision-modes.sh, and every provisioning mode delegates to the +# Justfile. This file is minted by `provision-set`; do not hand-edit it. +# Repo-specific facts belong in .machine_readable/descriptiles/provisioning_praxis.deed. +set -uo pipefail + +REPO_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" + +if [ ! -f "$REPO_DIR/build/just/provision-modes.sh" ]; then + echo "launcher.sh: build/just/provision-modes.sh is missing — this checkout is incomplete." >&2 + echo "Re-clone, or restore it: git checkout -- build/just/provision-modes.sh" >&2 + exit 1 +fi +# shellcheck source=build/just/provision-modes.sh +. "$REPO_DIR/build/just/provision-modes.sh" + +hp_launcher_main "$@" diff --git a/3-practice/provisioning/templates/llm-warmup-dev.adoc.tmpl b/3-practice/provisioning/templates/llm-warmup-dev.adoc.tmpl new file mode 100644 index 000000000..f2f8a8985 --- /dev/null +++ b/3-practice/provisioning/templates/llm-warmup-dev.adoc.tmpl @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: __DOC_LICENSE__ +// SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ += LLM warm-up — __APP_NAME__ (developer) + +Paste this into an AI assistant before changing code in __APP_NAME__. + +== Orientation + +__APP_NAME__ (https://github.com/__REPO_SLUG__): a __ARCHETYPE__ in __LANGS__. + +__SPEC_ARCHITECTURE__ + +== Toolchain + +* Declared in `mise.toml` (specs `latest`), made concrete in `mise.lock`. Never + hand-edit versions; `just toolchain-refresh` bumps and re-locks. +* Guix alternative: `manifest.scm` (dev shell), `guix.scm` (package), + `channels.scm` (pinned Guix). `just dev-shell` picks whichever is present. +* `just` ≥ 1.42. The shared recipes live in `build/just/provision.just` + (`mod provision`); root recipes in `Justfile` override them. + +== The loop + +[source,bash] +---- +just setup # once +just build +just test +just bench # N/A when there are no benchmarks +just eval # test + bench with timings, logged under .eval/ +just lint && just fmt +just doctor # before you open a PR +---- + +__SPEC_DEV_COMMANDS__ + +== Where things are + +__SPEC_LAYOUT__ + +== Rules that bite + +* Languages: bun is the JS runtime; Python, Deno, TypeScript, ReScript, Nix, + Go and Makefiles are banned. New code follows the repository's existing languages. +* Every new file carries an SPDX header: `__LICENSE__` for code and config, + `__DOC_LICENSE__` for prose docs. Never change an existing file's licence. +* Commits are signed; history is linear (squash merges). + +__SPEC_GOTCHAS__ diff --git a/3-practice/provisioning/templates/llm-warmup-maintainer.adoc.tmpl b/3-practice/provisioning/templates/llm-warmup-maintainer.adoc.tmpl new file mode 100644 index 000000000..62085cbbb --- /dev/null +++ b/3-practice/provisioning/templates/llm-warmup-maintainer.adoc.tmpl @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: __DOC_LICENSE__ +// SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ += LLM warm-up — __APP_NAME__ (maintainer) + +Paste this into an AI assistant before release, CI or governance work on +__APP_NAME__. Read `llm-warmup-dev.adoc` first; this adds what a maintainer owns. + +== The provisioning set + +This repository is provisioned from the estate canon +(https://github.com/hyperpolymath/standards, `3-practice/provisioning/`). +Repo-specific facts live in `.machine_readable/descriptiles/provisioning_praxis.deed`. +`provision-set realign` overwrites only the engine: `launcher.sh`, +`build/just/provision*.{just,sh}` and `channels.scm`; change behaviour through the +deed, never by hand-editing those. Everything else (`guix.scm` once real, +`manifest.scm`, `mise.toml`, the Justfile, `docs/` and these warm-ups) is minted +once and then belongs to this repository: keep it true as the code changes. + +== Keeping it current + +* `just toolchain-refresh` — `mise up --bump`, `mise lock`, re-pin `channels.scm`. + Commit the lock changes on their own. +* `just doctor` must be clean before a release; WARN codes are tracked, not ignored. +* The `provisioning-check` workflow reports drift from the canon. + +== Release + +__SPEC_RELEASE__ + +== CI and branch rules + +__SPEC_CI__ + +Rulesets can block a merge for reasons other than checks (review threads, +signatures, code owners): enumerate the ruleset's rule types before calling a PR +mergeable. + +== Decisions + +Owner rulings for the estate are recorded in `hyperpolymath/standards` +(issue #787 is the decision surface). Do not re-open a ruled question; cite it. +__SPEC_DECISIONS__ diff --git a/3-practice/provisioning/templates/llm-warmup-user.adoc.tmpl b/3-practice/provisioning/templates/llm-warmup-user.adoc.tmpl new file mode 100644 index 000000000..64f149cd7 --- /dev/null +++ b/3-practice/provisioning/templates/llm-warmup-user.adoc.tmpl @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: __DOC_LICENSE__ +// SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ += LLM warm-up — __APP_NAME__ (user) + +Paste this whole file into any AI assistant before asking it about +__APP_NAME__. It tells the assistant what the project is, how a person gets it +running, and where to look when something goes wrong. + +== What it is + +__APP_NAME__ (https://github.com/__REPO_SLUG__) is a __ARCHETYPE__ written in __LANGS__. + +__SPEC_WHAT_IT_IS__ + +== Getting it running + +The supported routes, in order of ease: + +. *Ask an AI*: follow `docs/AI_INSTALLATION_GUIDE.adoc` in the repository. +. *The launcher*: `git clone https://github.com/__REPO_SLUG__.git && cd __APP_NAME__ && ./launcher.sh --setup` +. *By hand*: `docs/SETUP.adoc`, every step explained. + +Prerequisites are git, a C toolchain and mise; everything else is installed by +`mise install`, at the versions pinned in `mise.lock`. + +== Using it + +__SPEC_USAGE__ + +== When something is wrong + +Run `just doctor`. Every FAIL line carries a code (PV-E…) that is explained, with +its fix, in `docs/SETUP.adoc` §Troubleshooting. `just heal` applies the automatic +fixes. Do not guess an installer that the repository does not document. + +== What it touches + +__SPEC_PRIVACY__ diff --git a/3-practice/provisioning/templates/mise.toml.tmpl b/3-practice/provisioning/templates/mise.toml.tmpl new file mode 100644 index 000000000..be4bc98a7 --- /dev/null +++ b/3-practice/provisioning/templates/mise.toml.tmpl @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: __LICENSE__ +# SPDX-FileCopyrightText: __YEAR__ __COPYRIGHT_HOLDER__ +# +# The toolchain for __APP_NAME__ (__LANGS__). Specs are `latest`; mise.lock +# records the concrete version and per-platform checksum of each, so every +# checkout installs the same bits. Refresh both with: just toolchain-refresh +# Canon: hyperpolymath/standards 3-practice/provisioning (TRUST-DEFAULTS §toolchain). +# Estate policy: no python, deno, node/npm/yarn, make or nix here; the JS runtime is bun. + +[settings] +lockfile = true + +[tools] +__MISE_TOOLS_TOML__ diff --git a/docs/UX-standards/launcher-standard.adoc b/docs/UX-standards/launcher-standard.adoc index 76c547a4b..15917f930 100644 --- a/docs/UX-standards/launcher-standard.adoc +++ b/docs/UX-standards/launcher-standard.adoc @@ -323,6 +323,95 @@ case "$MODE" in esac ---- +[[archetypes-and-provisioning]] +== Archetypes and Provisioning Modes (standard 0.5.0) + +Since standard version 0.5.0 (2026-09-30) *every* repository carries a +`launcher.sh`, not only desktop applications. The reason is simple: the +launcher is the one file a newcomer is told to run, so it must also be the +one file that gets them from a fresh clone to a working environment. + +=== Archetypes + +The repository declares its archetype in +`.machine_readable/descriptiles/provisioning_praxis.deed` as `(archetype "...")`. +The archetype decides which mode families apply: + +[cols="1,2,2",options="header"] +|=== +| Archetype | Required mode families | Not applicable +| `app` | runtime, integration, meta, provisioning | — +| `tool` | meta, provisioning | runtime, integration +| `library` | meta, provisioning | runtime, integration +| `theory` | meta, provisioning | runtime, integration +| `docs` | meta, provisioning | runtime, integration +|=== + +A "not applicable" mode is still *accepted*. `./launcher.sh --start` on a +library prints one line such as `mylib is a library: there is nothing to +start. Try --setup, --doctor or --help.` and exits 0. The user learns why, +instead of meeting a usage error. + +=== Provisioning Modes (required for every archetype) + +[cols="1,3"] +|=== +| Mode | Purpose + +| `--setup` +| Install everything the repository needs: the mise toolchain, the language + dependencies, and any repo-specific steps. It finishes by running `--doctor`. + +| `--doctor` +| Diagnose the environment. Output is `PASS`/`WARN`/`FAIL` lines, each with a + stable code (`PV-E10`, `PV-W30`…) that `docs/SETUP.adoc` §Troubleshooting + explains. Exit codes: `0` means no FAIL, `1` means at least one FAIL, `2` + means a usage error. + +| `--heal` +| Apply only safe, reversible fixes (install missing tools, trust the mise + config, restore executable bits, re-fetch dependencies), then re-run + `--doctor`. It never edits source, never rewrites history, and never removes + migration debt such as Deno or Nix files. Those are tracked as issues. + +| `--ai-setup` +| Print, and copy to the clipboard, the one-line "Just Say It" instruction + to give any AI assistant, plus the path to `docs/AI_INSTALLATION_GUIDE.adoc`. + It is vendor-neutral. +|=== + +Each provisioning mode *calls the provisioning engine directly* +(`bash build/just/provision-lib.sh setup|doctor|heal|ai-setup`). It does not +call a `just` recipe, so a repository's own root `doctor`, `setup` or `heal` +recipe can never shadow the canon. The engine needs only bash. `--setup` +installs `just` and the rest of the toolchain through mise, and prints the +one-line mise install when mise itself is missing. `just doctor` and +`./launcher.sh --doctor` run the same engine and give the same result. + +A repository keeps its own checks as root recipes named `doctor-local`, +`setup-local` and `heal-local`. The engine runs them, and a failing +`doctor-local` is FAIL `PV-E50`. + +The shared recipes live in the `build/just/provision.just` module. The full +contract, including `dev-shell`, `toolchain-refresh`, `eval`, `ai-warmup` and +the Guix and mise requirements, is in +link:../../3-practice/provisioning/PROVISIONING-STANDARD.adoc[PROVISIONING-STANDARD.adoc]. +Its machine-readable form is +`3-practice/provisioning/provisioning-standard_praxis.deed`. + +.Provisioning compliance checklist (all archetypes) +* [ ] `launcher.sh` exists at the repository root and is executable +* [ ] `--help` and `--version` exit 0 +* [ ] `--setup`, `--doctor`, `--heal` and `--ai-setup` are accepted and call `build/just/provision-lib.sh` +* [ ] runtime modes on a non-`app` archetype print the archetype line and exit 0 +* [ ] `--doctor` exits non-zero when any check FAILs + +Reference implementation: +link:../../3-practice/provisioning/templates/launcher.sh.tmpl[`3-practice/provisioning/templates/launcher.sh.tmpl`]. +It is also the base template for the library, tool, theory and docs +archetypes. For `app`, `launch-scaffolder` generates the runtime modes and +sources the same provisioning block. + == System Integration Modes (`--integ` / `--disinteg`) The `--integ` mode installs the launcher as a first-class desktop application diff --git a/guix.scm b/guix.scm index 8533f7212..59267535d 100644 --- a/guix.scm +++ b/guix.scm @@ -17,4 +17,4 @@ (synopsis "standards") (description "standards — part of the hyperpolymath ecosystem.") (home-page "https://github.com/hyperpolymath/standards") - (license ((@@ (guix licenses) license) "MPL-2.0" "https://github.com/hyperpolymath/palimpsest-license"))) + (license mpl2.0)) diff --git a/launcher/launcher-standard_praxis.deed b/launcher/launcher-standard_praxis.deed index ba3421e33..d444cb990 100644 --- a/launcher/launcher-standard_praxis.deed +++ b/launcher/launcher-standard_praxis.deed @@ -21,9 +21,12 @@ ;; The standard's OWN version, distinct from :schema-version (the DEED ;; format). Bumped 0.3.0 -> 0.4.0 because the resolution ladders below now - ;; name the .deed file, which is consumer-visible. - :standard-version "0.4.0" - :standard-date "2026-09-22" + ;; name the .deed file, which is consumer-visible. Bumped 0.4.0 -> 0.5.0 + ;; (2026-09-30): every repository now carries a launcher, profiled by + ;; archetype, with a mandatory provisioning mode family (see `archetypes` + ;; and `provisioning-modes` below). + :standard-version "0.5.0" + :standard-date "2026-09-30" :compliance ("launcher-standard.adoc" "LM-LA-LIFECYCLE-STANDARD.adoc" "cross-platform-system-integration-modes" @@ -257,6 +260,42 @@ (tool :name panic-attack :style command :trigger on-start-failed :command "panic-attack assail {repo-dir}")) + ;; ---------------------------------------------------------------- archetypes + ;; Since 0.5.0 EVERY repository carries launcher.sh, not only desktop apps. + ;; The archetype (declared in .machine_readable/descriptiles/provisioning_praxis.deed + ;; as `(archetype "...")`) decides which mode families apply. A mode family + ;; marked :not-applicable MUST still be accepted on the command line: the + ;; launcher prints one line naming the archetype and exits 0, so a user who + ;; types `--start` on a library is told why, not handed a usage error. + ;; A set, not a ladder. + (archetypes + (archetype :name app :required (runtime integration meta provisioning)) + (archetype :name tool :required (meta provisioning) + :not-applicable (runtime integration)) + (archetype :name library :required (meta provisioning) + :not-applicable (runtime integration)) + (archetype :name theory :required (meta provisioning) + :not-applicable (runtime integration)) + (archetype :name docs :required (meta provisioning) + :not-applicable (runtime integration))) + + ;; -------------------------------------------------------- provisioning-modes + ;; Required for ALL archetypes. Each mode calls the provisioning engine + ;; directly (`bash build/just/provision-lib.sh `), not a `just` + ;; recipe, so a repository's own root `doctor`/`setup`/`heal` recipe can + ;; never shadow the canon. The engine needs only bash; `--setup` installs + ;; just and the rest of the toolchain through mise. A repository's own + ;; checks live in `doctor-local`/`setup-local`/`heal-local`, which the + ;; engine runs; a failing doctor-local is FAIL PV-E50. + ;; Full contract: 3-practice/provisioning/PROVISIONING-STANDARD.adoc and + ;; 3-practice/provisioning/provisioning-standard_praxis.deed. + (provisioning-modes + :modes ("--setup" "--doctor" "--heal" "--ai-setup") + :delegate-to "build/just/provision-lib.sh" + :local-hooks ("doctor-local" "setup-local" "heal-local") + :reference-impl "3-practice/provisioning/templates/launcher.sh.tmpl" + (doctor-exit-codes :all-pass 0 :any-fail 1 :usage 2)) + ;; ------------------------------------------------------------ metadata-block ;; Every generated launcher must carry this metadata block in its header so ;; it can be re-parsed by `launch-scaffolder config` and `realign`. From e7b134e87250a50366079f3ba3034aeb24ea4572 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:26:20 +0100 Subject: [PATCH 02/20] fix(provisioning): detect ABI/FFI layout; launcher is generated - Idris2 and Zig are detected to depth 3 (src/abi/*.ipkg, ffi/zig/build.zig): 234 build.zig sit at that depth across the estate and 37 repos have their only Idris2/Zig marker there. Zig verbs now run in the build.zig directory. - launcher.sh is `generated`: realign re-renders it only when it carries the @launcher-deed block; hand-written launchers are kept and source provision-modes.sh. - A minted set whose deed :repo is another repository's is inherited (e.g. from rsr-template-repo) and is re-minted. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../provisioning/PROVISIONING-STANDARD.adoc | 12 ++++++-- .../provisioning-standard_praxis.deed | 14 ++++++---- .../templates/build/just/provision-lib.sh | 28 ++++++++++++------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index 98f9cb487..5a9889194 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -24,7 +24,7 @@ below, except forks, archived repositories, `007` and the two vaults |=== |File |Kind |Purpose -|`launcher.sh` |engine |`--setup`, `--doctor`, `--heal`, `--ai-setup`, plus runtime modes for the `app` archetype +|`launcher.sh` |generated |`--setup`, `--doctor`, `--heal`, `--ai-setup`, plus runtime modes for the `app` archetype |`build/just/provision.just` |engine |the `provision::` just module: every verb below |`build/just/provision-lib.sh` |engine |the implementation of every verb (bash only, shellcheck-clean) |`build/just/provision-modes.sh` |engine |the launcher's provisioning dispatch @@ -40,7 +40,15 @@ below, except forks, archived repositories, `007` and the two vaults |=== *Engine* files are identical estate-wide and `provision-set realign` overwrites -them. *Minted* files are written once, then owned by the repository: realign +them. The *generated* `launcher.sh` is rendered from the template and the repo's +deed. Realign re-renders it only when its header carries the `@launcher-deed` +block, which marks it as generated. A hand-written launcher is kept, and gets +the provisioning modes by sourcing `build/just/provision-modes.sh`. + +A minted set whose deed `:repo` differs from the checkout's own slug was +*inherited* (for example from `rsr-template-repo` through GitHub's "Use this +template"). Realign treats inherited files as stubs and re-mints them, so a +new repository is never born describing its template. *Minted* files are written once, then owned by the repository: realign creates them when missing and replaces them only while they are still stubs (doctor PV-W24–W28), never once filled. The Justfile is merged: an existing custom `doctor`, `setup` or `heal` recipe is renamed `doctor-local`, diff --git a/3-practice/provisioning/provisioning-standard_praxis.deed b/3-practice/provisioning/provisioning-standard_praxis.deed index 73ee49396..c6dde03e1 100644 --- a/3-practice/provisioning/provisioning-standard_praxis.deed +++ b/3-practice/provisioning/provisioning-standard_praxis.deed @@ -24,12 +24,16 @@ :exclude ("forks" "archived" "007" "dev-notes-vault" "memory-vault")) ;; ------------------------------------------------------------ artefact set - ;; engine = identical estate-wide; `provision-set realign` overwrites it. - ;; minted = written once, then owned by the repository; realign creates it - ;; when missing and replaces it only while it is still a stub. - ;; merged = the repository's own content is kept; the canon is added. + ;; engine = identical estate-wide; `provision-set realign` overwrites it. + ;; generated = rendered from the template + deed; realign re-renders it only + ;; when it carries the @launcher-deed block (hand-written ones are + ;; kept and source build/just/provision-modes.sh). + ;; minted = written once, then owned by the repository; realign creates it + ;; when missing and replaces it only while it is still a stub, or + ;; when the deed's :repo is another repository's (inherited). + ;; merged = the repository's own content is kept; the canon is added. (artefacts - (file :path "launcher.sh" :kind engine :mode "100755") + (file :path "launcher.sh" :kind generated :mode "100755") (file :path "build/just/provision.just" :kind engine) (file :path "build/just/provision-lib.sh" :kind engine :mode "100755") (file :path "build/just/provision-modes.sh" :kind engine :mode "100755") diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index 9eed95eca..96610f3ad 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -54,15 +54,17 @@ REPO_NAME="${REPO_SLUG#*/}" ARCHETYPE="$(deed archetype "")" # --------------------------------------------------------------------------- -# Language detection. Markers at the root or one level down (workspaces). +# Language detection. Markers at the root or one level down (workspaces); Idris2 +# and Zig to two levels down, because the estate ABI/FFI pattern puts them at +# src/abi/*.ipkg and ffi/zig/build.zig (234 build.zig at that depth, 2026-09-30). # Order matters only for display. `docs` is reported when nothing else is. first() { find . -maxdepth "${2:-2}" -not -path './.git/*' -not -path '*/node_modules/*' -not -path './target/*' -name "$1" -print -quit 2>/dev/null; } detect_langs() { local out=() [ -n "$(first Cargo.toml)" ] && out+=(rust) - [ -n "$(first '*.ipkg')" ] && out+=(idris2) + [ -n "$(first '*.ipkg' 3)" ] && out+=(idris2) [ -n "$(first Project.toml 1)" ] && out+=(julia) - [ -n "$(first build.zig)" ] && out+=(zig) + [ -n "$(first build.zig 3)" ] && out+=(zig) [ -n "$(first mix.exs)" ] && out+=(elixir) [ -n "$(first gleam.toml)" ] && out+=(gleam) [ -n "$(first dune-project)" ] && out+=(ocaml) @@ -166,7 +168,7 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) rust:run) echo "cargo run --release" ;; idris2:*) - ipkg=$(first '*.ipkg' 1); ipkg=${ipkg#./}; [ -z "$ipkg" ] && ipkg=$(first '*.ipkg'); ipkg=${ipkg#./} + ipkg=$(first '*.ipkg' 1); [ -z "$ipkg" ] && ipkg=$(first '*.ipkg'); [ -z "$ipkg" ] && ipkg=$(first '*.ipkg' 3); ipkg=${ipkg#./} case "$v" in deps) have pack && echo "pack install-deps $ipkg" ;; build) have pack && echo "pack build $ipkg" || echo "idris2 --build $ipkg" ;; @@ -181,12 +183,18 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) julia:test) echo "julia --project=. -e 'using Pkg; Pkg.test()'" ;; julia:bench) [ -f benchmark/benchmarks.jl ] && echo "julia --project=benchmark -e 'using Pkg; Pkg.develop(path=\".\"); Pkg.instantiate(); include(\"benchmark/benchmarks.jl\")'" ;; - zig:build) echo "zig build" ;; - zig:test) echo "zig build test" ;; - zig:bench) grep -qs '"bench"' build.zig && echo "zig build bench -Doptimize=ReleaseFast" ;; - zig:fmt) echo "zig fmt ." ;; - zig:lint) echo "zig fmt --check ." ;; - zig:run) grep -qs '"run"' build.zig && echo "zig build run" ;; + zig:*) + # build.zig may sit in ffi/zig/; run there, not at the root. + local zb zd; zb=$(first build.zig 1); [ -z "$zb" ] && zb=$(first build.zig); [ -z "$zb" ] && zb=$(first build.zig 3) + zd=$(dirname "${zb#./}"); local cdz=""; [ "$zd" != . ] && cdz="cd '$zd' && " + case "$v" in + build) echo "${cdz}zig build" ;; + test) echo "${cdz}zig build test" ;; + bench) grep -qs '"bench"' "$zb" && echo "${cdz}zig build bench -Doptimize=ReleaseFast" ;; + fmt) echo "zig fmt $zd" ;; + lint) echo "zig fmt --check $zd" ;; + run) grep -qs '"run"' "$zb" && echo "${cdz}zig build run" ;; + esac ;; elixir:deps) echo "mix local.hex --force --if-missing && mix local.rebar --force --if-missing && mix deps.get" ;; elixir:build) echo "mix compile --warnings-as-errors" ;; From 40010efb6dce804aac74ca16e9caf5d5b782a9a1 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:30:56 +0100 Subject: [PATCH 03/20] fix(provisioning): no curl|sh install hint; regenerate registry Hypatia code_safety flagged the mise install hint in provision-modes.sh as download-then-run (CWE-494, high, new). Every mise install hint now leads with the OS package manager (brew, Fedora COPR, winget, mise's install docs) and gives the installer only as download, read, run -- never piped into a shell. One MISE_INSTALL_HINT in provision-lib.sh feeds both doctor and setup messages; SETUP and the AI guide match, and the AI guide says to show the installer to the user before running it. The two `eval "$(mise env -s bash)"` sites now prepend `mise bin-paths` to PATH instead, which is what they needed and needs no eval. REGISTRY.a2ml was stale (RSR source_hash): regenerated with scripts/build-registry.sh, which also fixes build-registry-test.sh (9 passed, 0 failed locally). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .machine_readable/REGISTRY.a2ml | 2 +- .../templates/build/just/provision-lib.sh | 11 +++++++---- .../templates/build/just/provision-modes.sh | 8 ++++++-- .../templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl | 7 ++++++- .../provisioning/templates/docs/SETUP.adoc.tmpl | 2 +- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.machine_readable/REGISTRY.a2ml b/.machine_readable/REGISTRY.a2ml index 763f73517..82172235a 100644 --- a/.machine_readable/REGISTRY.a2ml +++ b/.machine_readable/REGISTRY.a2ml @@ -207,7 +207,7 @@ name = "RSR — Rhodium Standard Repositories" stream = "governance" home = "rhodium-standard-repositories/" canonical_doc = "rhodium-standard-repositories/README.adoc" -source_hash = "sha256:bc9c99e1d48f9b6ca6985fc705976ed0fee560d60c1d3259e1647a382b76a1e1" +source_hash = "sha256:51b38eb327df7ea430078bd75566d15ab9a6618f19a3b9ee7c2a1cfb8ce6adda" route = "the repository-compliance standard every repo is graded against" [[spec]] diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index 96610f3ad..ba39c014b 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -25,6 +25,8 @@ ROOT="${PROVISION_ROOT:-$(pwd)}" cd "$ROOT" || exit 2 DEED=".machine_readable/descriptiles/provisioning_praxis.deed" MIN_JUST="1.42.0" # root→module recipe deps (`doctor: provision::doctor`); 1.41 rejects them +# How to get mise: package managers first; the installer only as download, read, run. +MISE_INSTALL_HINT="brew install mise | Fedora: dnf copr enable jdxcode/mise, dnf install mise | winget install jdx.mise | others: https://mise.jdx.dev/installing-mise.html — or: curl -fsSLo mise-install.sh https://mise.run, read it, sh mise-install.sh" T="timeout 15" # some --version probes hang (observed 2026-09-30); never probe unbounded if [ -t 1 ]; then R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[34m'; Z=$'\033[0m'; else R=; G=; Y=; B=; Z=; fi @@ -141,7 +143,7 @@ mise_tools() { lang_remedy() { case "$1" in - rust) echo "mise use rust@latest (or: curl https://sh.rustup.rs | sh)" ;; + rust) echo "mise use rust@latest (or rustup: https://rustup.rs)" ;; idris2) echo "install pack: https://github.com/stefan-hoeck/idris2-pack#installation, then: pack install-app idris2" ;; julia) echo "mise use julia@latest (or: https://julialang.org/install/ — juliaup)" ;; zig) echo "mise use zig@latest" ;; @@ -284,7 +286,7 @@ cmd_doctor() { missing=$($T mise ls --current --missing 2>/dev/null | grep -F -e "$here/mise.toml" -e "$here/.mise.toml" -e "$ROOT/mise.toml" | awk '{print $1"@"$2}' | tr '\n' ' ') [ -z "${missing// /}" ] && pass "every mise tool is installed" || fail "PV-E03 mise tools not installed: $missing— run: just setup" fi - else warn "PV-W01 mise not found — tools must then come from Guix or your OS; install: curl https://mise.run | sh"; fi + else warn "PV-W01 mise not found — tools must then come from Guix or your OS; install: $MISE_INSTALL_HINT"; fi if have guix; then pass "guix $($T guix --version 2>/dev/null | head -1 | awk '{print $NF}') (optional reproducible path)" else info "guix not installed — optional; mise is the default path (docs/SETUP.adoc §Guix)"; fi @@ -362,7 +364,7 @@ cmd_setup() { $T mise trust -q . 2>/dev/null || true mise install || rc=1 else - warn "mise not found. Install it (curl https://mise.run | sh), or enter 'guix shell -m manifest.scm', then re-run: just setup" + warn "mise not found. Install it ($MISE_INSTALL_HINT), or enter 'guix shell -m manifest.scm', then re-run: just setup" fi local l for l in "${LANGS[@]}"; do @@ -373,7 +375,8 @@ cmd_setup() { esac done hdr "Project dependencies" - if have mise; then eval "$(mise env -s bash 2>/dev/null)" || true; fi + # Put this repo's mise tools on PATH for the dependency step. + if have mise; then PATH="$(mise bin-paths 2>/dev/null | paste -sd: -):$PATH"; export PATH; fi lang_run deps || rc=1 local hook=build/just/setup-local.sh [ -f "$hook" ] && { hdr "Repo-specific setup ($hook)"; bash "$hook" || rc=1; } diff --git a/3-practice/provisioning/templates/build/just/provision-modes.sh b/3-practice/provisioning/templates/build/just/provision-modes.sh index 8e2a5e83c..96ebb5eaf 100755 --- a/3-practice/provisioning/templates/build/just/provision-modes.sh +++ b/3-practice/provisioning/templates/build/just/provision-modes.sh @@ -43,14 +43,18 @@ hp_ensure_just() { command -v just >/dev/null 2>&1 && return 0 if command -v mise >/dev/null 2>&1; then echo "just is not installed — installing it with mise (mise use -g just@latest)..." >&2 - mise use -g just@latest >&2 && eval "$(mise env -s bash 2>/dev/null)" && command -v just >/dev/null 2>&1 && return 0 + mise use -g just@latest >&2 && PATH="$(mise bin-paths 2>/dev/null | paste -sd: -):$PATH" && command -v just >/dev/null 2>&1 && return 0 mise exec just@latest -- true >/dev/null 2>&1 && { just() { mise exec just@latest -- just "$@"; }; return 0; } fi cat >&2 <<'EOF' Neither `just` nor `mise` is installed. Install mise (it then installs everything else): - curl https://mise.run | sh # Linux / macOS / WSL + brew install mise # macOS + sudo dnf copr enable jdxcode/mise && sudo dnf install mise # Fedora winget install jdx.mise # Windows + # anything else (Debian/Ubuntu apt, …): https://mise.jdx.dev/installing-mise.html + # or download the installer, read it, then run it — never pipe it into a shell: + curl -fsSLo mise-install.sh https://mise.run && less mise-install.sh && sh mise-install.sh then re-run this command. Manual route without mise: docs/SETUP.adoc EOF diff --git a/3-practice/provisioning/templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl b/3-practice/provisioning/templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl index ca41c5732..9b58c2dbd 100644 --- a/3-practice/provisioning/templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl +++ b/3-practice/provisioning/templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl @@ -58,7 +58,12 @@ mise --version # any version ---- * Missing git: Fedora `sudo dnf install git` · Debian/Ubuntu `sudo apt install git` · macOS `xcode-select --install`. -* Missing mise: `curl https://mise.run | sh`, then run the activation line it +* Missing mise: use the OS package manager (macOS `brew install mise` · Fedora + `sudo dnf copr enable jdxcode/mise && sudo dnf install mise` · Windows + `winget install jdx.mise` · others: https://mise.jdx.dev/installing-mise.html). + Only if none fits: download the installer, show it to the user, and run it + once they agree: `curl -fsSLo mise-install.sh https://mise.run && sh mise-install.sh`. + Never pipe a download into a shell. Then run the activation line it prints (e.g. `echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc`) and open a new shell. * A C toolchain, if `cc --version` fails: Fedora `sudo dnf install gcc` · Debian/Ubuntu `sudo apt install build-essential`. diff --git a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl index 60c3f608a..9d6fd23cd 100644 --- a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl +++ b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl @@ -36,7 +36,7 @@ for you in step 3. |mise |installs every other tool at the version this repo declares -|Linux/macOS/WSL `curl https://mise.run \| sh` · Windows `winget install jdx.mise` — then follow the one line it prints to activate it in your shell +|macOS `brew install mise` · Fedora `sudo dnf copr enable jdxcode/mise && sudo dnf install mise` · Windows `winget install jdx.mise` · Debian/Ubuntu and the rest: https://mise.jdx.dev/installing-mise.html — or download the installer, read it, then run it: `curl -fsSLo mise-install.sh https://mise.run && less mise-install.sh && sh mise-install.sh`. Then follow the one line it prints to activate it in your shell |=== NOTE: On Windows, use WSL2 (`wsl --install`) and follow the Linux steps inside From 206eb6c422b59054ae7f46b16a63176340dd950f Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:17:36 +0100 Subject: [PATCH 04/20] feat(provisioning): one placement resolver; zig found 3 dirs down A file sits at the root only when a tool needs it there. The Guix trio and the warm-ups follow the repository's existing layout: root, or build/ (guix) and docs/onboarding/ or docs/ (warm-ups), as rsr-template-repo already does. provision-lib.sh owns the answer (guix-dir, set-files); doctor, dev-shell, toolchain-refresh and provision-check.sh all ask it, so the engine and its checker cannot disagree about where a file lives. Both guix.scm and build/guix.scm present is the new PV-W35. Zig detection now reaches depth 4 (src/interface/ffi/build.zig): 74 repos keep their only build.zig there (measured 2026-09-30). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../provisioning/PROVISIONING-STANDARD.adoc | 18 +++-- .../provisioning-standard_praxis.deed | 8 ++- .../templates/build/just/provision-check.sh | 22 +++--- .../templates/build/just/provision-lib.sh | 72 +++++++++++++------ .../templates/docs/SETUP.adoc.tmpl | 1 + 5 files changed, 87 insertions(+), 34 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index 5a9889194..dae3eb4b0 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -28,17 +28,27 @@ below, except forks, archived repositories, `007` and the two vaults |`build/just/provision.just` |engine |the `provision::` just module: every verb below |`build/just/provision-lib.sh` |engine |the implementation of every verb (bash only, shellcheck-clean) |`build/just/provision-modes.sh` |engine |the launcher's provisioning dispatch -|`channels.scm` |engine |the pinned Guix commit +|`channels.scm` |engine |the pinned Guix commit (placed beside `guix.scm`, see below) |`Justfile` |merged |`mod provision` plus root delegations; the repo's own recipes are kept |`mise.toml` + `mise.lock` |minted |the toolchain: `latest` in `mise.toml`, made concrete and checksummed in `mise.lock` -|`guix.scm`, `manifest.scm` |minted |the Guix package and development shell +|`guix.scm`, `manifest.scm` |minted |the Guix package and development shell (at the root, or all three under `build/` where the repository already keeps `build/guix.scm`) |`.machine_readable/descriptiles/provisioning_praxis.deed` |minted |this repo's facts: archetype, languages, verb overrides, system deps, ports, config |`docs/SETUP.adoc` |minted |the complete manual route, with the doctor-code troubleshooting table |`docs/AI_INSTALLATION_GUIDE.adoc` |minted |the steps an AI assistant follows -|`llm-warmup-{user,dev,maintainer}.adoc` |minted |paste-in context for any AI, one per audience +|`llm-warmup-{user,dev,maintainer}.adoc` |minted |paste-in context for any AI, one per audience (at the root, or in `docs/onboarding/` or `docs/` where the repository already keeps them) |README `[[ai-install]]` section |inserted |"Just say it": one sentence to give any AI |=== +*Placement.* A file sits at the root only when something needs it there: +`launcher.sh`, `Justfile`, `mise.toml` and `mise.lock` are found by the tools that +read them. The Guix trio and the warm-ups follow the layout the repository already +has, so `rsr-template-repo` keeps `build/guix.scm` and +`docs/onboarding/llm-warmup-*.adoc`. One resolver in `provision-lib.sh` decides +this (`provision-lib.sh guix-dir` and `set-files`), and doctor, `dev-shell`, +`toolchain-refresh` and `provision-check.sh` all ask it, so no two of them can +disagree about where a file lives. Both `guix.scm` and `build/guix.scm` present is +PV-W35. + *Engine* files are identical estate-wide and `provision-set realign` overwrites them. The *generated* `launcher.sh` is rendered from the template and the repo's deed. Realign re-renders it only when its header carries the `@launcher-deed` @@ -67,7 +77,7 @@ of the same name can never shadow them. |`setup` |`mise install`, the repo's dependencies per language, `setup-local`, then `doctor` |`doctor` |PASS / WARN / FAIL for every requirement, each with a code and its fix; the last line is `: N PASS, N WARN, N FAIL`; exits non-zero if and only if something FAILed |`heal` |applies every fix marked *auto*, runs `heal-local`, then `doctor` -|`dev-shell` |`guix shell -m manifest.scm` when Guix is present, otherwise the mise environment +|`dev-shell` |`guix shell -m /manifest.scm` when Guix is present, otherwise the mise environment |`toolchain-refresh` |`mise up --bump`, re-lock `mise.lock`, re-pin `channels.scm` (the TRUST-DEFAULTS-POLICY recipe) |`build`, `test`, `bench`, `lint`, `fmt`, `run`, `deps` |the deed's override, else the language default, else an explicit "N/A for this archetype" and exit 0; nothing is faked |`eval` |`test` + `bench` with timings, saved under `.eval/` diff --git a/3-practice/provisioning/provisioning-standard_praxis.deed b/3-practice/provisioning/provisioning-standard_praxis.deed index c6dde03e1..c55338741 100644 --- a/3-practice/provisioning/provisioning-standard_praxis.deed +++ b/3-practice/provisioning/provisioning-standard_praxis.deed @@ -50,6 +50,12 @@ (file :path "llm-warmup-user.adoc" :kind minted) (file :path "llm-warmup-dev.adoc" :kind minted) (file :path "llm-warmup-maintainer.adoc" :kind minted) + ;; placement: the Guix trio lives beside guix.scm (root, or build/ when the + ;; repository already has build/guix.scm); warm-ups at the root, or in + ;; docs/onboarding/ or docs/ when already kept there. Resolved by + ;; `provision-lib.sh guix-dir` / `set-files`, shared by doctor and check. + (placement :root-required ("launcher.sh" "Justfile" "mise.toml" "mise.lock") + :guix-dir ("." "build") :warmup-dir ("." "docs/onboarding" "docs")) (section :file "README.adoc" :anchor "ai-install" :kind merged)) ;; ------------------------------------------------------------------- verbs @@ -117,4 +123,4 @@ :exit "non-zero iff any FAIL" :fail-codes ("PV-E01" "PV-E02" "PV-E03" "PV-E10" "PV-E11" "PV-E20" "PV-E27" "PV-E40" "PV-E50") :warn-codes ("PV-W01" "PV-W20" "PV-W21" "PV-W22" "PV-W23" "PV-W24" "PV-W25" "PV-W26" - "PV-W27" "PV-W28" "PV-W29" "PV-W30" "PV-W31" "PV-W32" "PV-W33" "PV-W34"))) + "PV-W27" "PV-W28" "PV-W29" "PV-W30" "PV-W31" "PV-W32" "PV-W33" "PV-W34" "PV-W35"))) diff --git a/3-practice/provisioning/templates/build/just/provision-check.sh b/3-practice/provisioning/templates/build/just/provision-check.sh index 1e438edcd..7fbb808e3 100755 --- a/3-practice/provisioning/templates/build/just/provision-check.sh +++ b/3-practice/provisioning/templates/build/just/provision-check.sh @@ -12,6 +12,8 @@ # for a checkout mid-specialisation. Mechanical residue (__X__) always fails. set -uo pipefail +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +LIB="$HERE/provision-lib.sh" DEV=0 [ "${1:-}" = "--dev" ] && { DEV=1; shift; } REPO="${1:-.}" @@ -24,10 +26,13 @@ BANNED="python deno node nodejs npm yarn pnpm go golang java kotlin make black r # The engine scripts under build/just/ are excluded: they name slot syntax in their # own comments and code, and realign owns them. README.adoc is scanned only inside # its [[ai-install]] section. -SET_FILES="launcher.sh Justfile justfile mise.toml guix.scm manifest.scm channels.scm - .machine_readable/descriptiles/provisioning_praxis.deed docs/SETUP.adoc - docs/AI_INSTALLATION_GUIDE.adoc llm-warmup-user.adoc llm-warmup-dev.adoc - llm-warmup-maintainer.adoc" +# Their locations come from provision-lib.sh (guix-dir, set-files): the engine and +# this check must never disagree about where the Guix files or warm-ups live. +[ -f "$LIB" ] || { echo "provision-check: engine missing: $LIB" >&2; exit 1; } +lib() { PROVISION_ROOT="$PWD" bash "$LIB" "$@"; } +GDIR=$(lib guix-dir) +SET_FILES=$(lib set-files) +gp() { [ "$GDIR" = . ] && echo "$1" || echo "$GDIR/$1"; } fails=0 warns=0 ok() { printf ' ok %s\n' "$*"; } @@ -80,10 +85,11 @@ else fi echo "[4] guix" -if [ ! -f guix.scm ]; then bad "guix.scm missing" -elif grep -qE '\{\{|\(inputs \(list\)\)|\(source #f\)' guix.scm; then bad "guix.scm is a stub" -else ok "guix.scm is not a stub"; fi -for g in manifest.scm channels.scm; do [ -f "$g" ] || bad "$g missing"; done +gs=$(gp guix.scm) +if [ ! -f "$gs" ]; then bad "guix.scm missing (neither guix.scm nor build/guix.scm)" +elif grep -qE '\{\{|\(inputs \(list\)\)|\(source #f\)' "$gs"; then bad "$gs is a stub" +else ok "$gs is not a stub"; fi +for g in "$(gp manifest.scm)" "$(gp channels.scm)"; do [ -f "$g" ] || bad "$g missing"; done echo "[5] template residue" f0=$fails diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index ba39c014b..762ea6f9d 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -38,6 +38,29 @@ info() { printf ' %sinfo%s %s\n' "$B" "$Z" "$*"; } hdr() { printf '\n%s== %s ==%s\n' "$B" "$*" "$Z"; } have() { command -v "$1" >/dev/null 2>&1; } +# Where this repository keeps each part of the set. A file sits at the root only +# when something needs it there (launcher.sh, Justfile, mise.toml, mise.lock); +# the Guix trio and the warm-ups follow the layout the repository already has +# (rsr-template-repo keeps build/guix.scm and docs/onboarding/llm-warmup-*.adoc). +# This is the ONE answer: provision-check.sh asks it through `guix-dir` and +# `set-files` rather than keeping its own copy. +guix_dir() { if [ -f build/guix.scm ]; then echo build; else echo .; fi; } +warmup_dir() { + local d + for d in . docs/onboarding docs; do + compgen -G "$d/llm-warmup-*.adoc" >/dev/null && { echo "$d"; return; } + done + echo . +} +gpath() { local d; d=$(guix_dir); [ "$d" = . ] && echo "$1" || echo "$d/$1"; } +wpath() { local d; d=$(warmup_dir); [ "$d" = . ] && echo "llm-warmup-$1.adoc" || echo "$d/llm-warmup-$1.adoc"; } +set_files() { + printf '%s\n' launcher.sh Justfile justfile mise.toml \ + "$(gpath guix.scm)" "$(gpath manifest.scm)" "$(gpath channels.scm)" \ + "$DEED" docs/SETUP.adoc docs/AI_INSTALLATION_GUIDE.adoc \ + "$(wpath user)" "$(wpath dev)" "$(wpath maintainer)" +} + # --------------------------------------------------------------------------- # Descriptor: flat `:key "value"` reads from provisioning_praxis.deed (s-expression). deed() { # $1 key, $2 default @@ -56,9 +79,10 @@ REPO_NAME="${REPO_SLUG#*/}" ARCHETYPE="$(deed archetype "")" # --------------------------------------------------------------------------- -# Language detection. Markers at the root or one level down (workspaces); Idris2 -# and Zig to two levels down, because the estate ABI/FFI pattern puts them at -# src/abi/*.ipkg and ffi/zig/build.zig (234 build.zig at that depth, 2026-09-30). +# Language detection. Markers at the root or one level down (workspaces). The +# estate ABI/FFI pattern nests deeper: Idris2 to two levels down (src/abi/*.ipkg), +# Zig to three (ffi/zig/build.zig, and rsr-template-repo's src/interface/ffi/build.zig: +# 74 repos have their only build.zig at that depth, measured 2026-09-30). # Order matters only for display. `docs` is reported when nothing else is. first() { find . -maxdepth "${2:-2}" -not -path './.git/*' -not -path '*/node_modules/*' -not -path './target/*' -name "$1" -print -quit 2>/dev/null; } detect_langs() { @@ -66,7 +90,7 @@ detect_langs() { [ -n "$(first Cargo.toml)" ] && out+=(rust) [ -n "$(first '*.ipkg' 3)" ] && out+=(idris2) [ -n "$(first Project.toml 1)" ] && out+=(julia) - [ -n "$(first build.zig 3)" ] && out+=(zig) + [ -n "$(first build.zig 4)" ] && out+=(zig) [ -n "$(first mix.exs)" ] && out+=(elixir) [ -n "$(first gleam.toml)" ] && out+=(gleam) [ -n "$(first dune-project)" ] && out+=(ocaml) @@ -150,7 +174,7 @@ lang_remedy() { elixir) echo "mise use erlang@latest elixir@latest (erlang builds from source: allow ~10 min)" ;; gleam) echo "mise use gleam@latest erlang@latest" ;; ocaml) echo "mise use opam@latest && opam init -y && opam switch create . --deps-only -y" ;; - haskell) echo "ghcup: https://www.haskell.org/ghcup/ (or: guix shell -m manifest.scm)" ;; + haskell) echo "ghcup: https://www.haskell.org/ghcup/ (or: just dev-shell, which uses Guix)" ;; bun) echo "mise use bun@latest" ;; esac } @@ -187,7 +211,7 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) zig:*) # build.zig may sit in ffi/zig/; run there, not at the root. - local zb zd; zb=$(first build.zig 1); [ -z "$zb" ] && zb=$(first build.zig); [ -z "$zb" ] && zb=$(first build.zig 3) + local zb zd; zb=$(first build.zig 1); [ -z "$zb" ] && zb=$(first build.zig); [ -z "$zb" ] && zb=$(first build.zig 3); [ -z "$zb" ] && zb=$(first build.zig 4) zd=$(dirname "${zb#./}"); local cdz=""; [ "$zd" != . ] && cdz="cd '$zd' && " case "$v" in build) echo "${cdz}zig build" ;; @@ -316,14 +340,16 @@ cmd_doctor() { for g in guix.scm build/guix.scm manifest.scm; do [ -f "$g" ] && grep -qE '\{\{|\(inputs \(list\)\)|\(source #f\)|__[A-Z][A-Z_]*__' "$g" && gstub="$gstub$g " done - if [ -f guix.scm ]; then + local gs gm; gs=$(gpath guix.scm); gm=$(gpath manifest.scm) + [ -f guix.scm ] && [ -f build/guix.scm ] && warn "PV-W35 both guix.scm and build/guix.scm exist — two Guix sources; keep one (build/ is used)" + if [ -f "$gs" ]; then if [ -n "$gstub" ]; then warn "PV-W24 template stub in: $gstub— the Guix path does not build this repo (just heal cannot fix this; provision-set realign does)" - else pass "guix.scm (non-stub)"; fi + else pass "$gs (non-stub)"; fi else warn "PV-W25 guix.scm missing — no reproducible Guix path"; fi - [ -f manifest.scm ] && pass "manifest.scm (guix shell -m manifest.scm)" || warn "PV-W26 manifest.scm missing — 'just dev-shell' falls back to mise" + [ -f "$gm" ] && pass "$gm (guix shell -m $gm)" || warn "PV-W26 $gm missing — 'just dev-shell' falls back to mise" [ -x launcher.sh ] && pass "launcher.sh (executable)" || { [ -f launcher.sh ] && fail "PV-E27 launcher.sh not executable — run: just heal" || warn "PV-W27 launcher.sh missing"; } local d - for d in docs/SETUP.adoc docs/AI_INSTALLATION_GUIDE.adoc llm-warmup-user.adoc llm-warmup-dev.adoc llm-warmup-maintainer.adoc; do + for d in docs/SETUP.adoc docs/AI_INSTALLATION_GUIDE.adoc "$(wpath user)" "$(wpath dev)" "$(wpath maintainer)"; do if [ ! -f "$d" ]; then warn "PV-W28 $d missing" elif grep -qE "__[A-Z][A-Z_]*__" "$d"; then warn "PV-W29 $d still has unfilled template slots (__SPEC_…__) — replace each with the facts for this repo" else pass "$d"; fi @@ -364,7 +390,7 @@ cmd_setup() { $T mise trust -q . 2>/dev/null || true mise install || rc=1 else - warn "mise not found. Install it ($MISE_INSTALL_HINT), or enter 'guix shell -m manifest.scm', then re-run: just setup" + warn "mise not found. Install it ($MISE_INSTALL_HINT), or enter the Guix shell with 'just dev-shell', then re-run: just setup" fi local l for l in "${LANGS[@]}"; do @@ -410,9 +436,10 @@ cmd_heal() { } cmd_dev_shell() { - if have guix && [ -f manifest.scm ] && ! grep -q '{{' manifest.scm; then - info "entering: guix shell -m manifest.scm (exit to leave)" - exec guix shell -m manifest.scm + local gm; gm=$(gpath manifest.scm) + if have guix && [ -f "$gm" ] && ! grep -q '{{' "$gm"; then + info "entering: guix shell -m $gm (exit to leave)" + exec guix shell -m "$gm" elif have mise; then info "guix/manifest.scm unavailable — entering the mise environment instead (exit to leave)" exec mise exec -- "${SHELL:-bash}" @@ -426,14 +453,15 @@ cmd_toolchain_refresh() { have mise || { fail "mise not found"; return 1; } mise up --bump || return 1 $T mise lock 2>/dev/null || mise lock || return 1 - if [ -f channels.scm ]; then + local ch; ch=$(gpath channels.scm) + if [ -f "$ch" ]; then hdr "guix: channel pin" if have guix; then - guix pull --channels=channels.scm --dry-run >/dev/null 2>&1 || true - guix describe --format=channels > channels.scm.new 2>/dev/null && mv channels.scm.new channels.scm && info "channels.scm re-pinned to the current guix commit" - else info "guix not installed — channels.scm left as-is (CI re-pins it)"; fi + guix pull --channels="$ch" --dry-run >/dev/null 2>&1 || true + guix describe --format=channels > "$ch.new" 2>/dev/null && mv "$ch.new" "$ch" && info "$ch re-pinned to the current guix commit" + else info "guix not installed — $ch left as-is (CI re-pins it)"; fi fi - git --no-pager diff --stat -- mise.toml mise.lock channels.scm 2>/dev/null + git --no-pager diff --stat -- mise.toml mise.lock "$ch" 2>/dev/null echo "Commit the diff above (signed) as: chore(toolchain): weekly refresh" } @@ -473,7 +501,7 @@ EOF cmd_ai_warmup() { local who=${1:-user} f case "$who" in user|dev|maintainer) ;; *) echo "usage: just ai-warmup " >&2; return 2 ;; esac - for f in "llm-warmup-$who.adoc" "docs/llm-warmup-$who.adoc"; do + for f in "$(wpath "$who")" "llm-warmup-$who.adoc" "docs/onboarding/llm-warmup-$who.adoc" "docs/llm-warmup-$who.adoc"; do if [ -f "$f" ]; then cat "$f"; clip < "$f" && echo "--- (copied $f to your clipboard — paste it as your first message to any AI)" >&2 || true return 0 @@ -511,7 +539,7 @@ cmd_config_show() { echo "run: $(deed run "$(for l in "${LANGS[@]}"; do lang_cmd "$l" run; done | head -1)")" echo "ports: $(deed ports "none")" echo "mise: $([ -f mise.toml ] && echo mise.toml) $([ -f mise.lock ] && echo mise.lock)" - echo "guix: $(for f in guix.scm manifest.scm channels.scm; do [ -f "$f" ] && printf "%s " "$f"; done)" + echo "guix: $(for f in guix.scm manifest.scm channels.scm; do f=$(gpath "$f"); [ -f "$f" ] && printf "%s " "$f"; done)" echo "lib: provision-lib $PROVISION_LIB_VERSION" } @@ -561,5 +589,7 @@ case "${1:-}" in search) shift; cmd_search "${1:-}" ;; lang-run) shift; lang_run "${1:?verb}" ;; version) echo "provision-lib $PROVISION_LIB_VERSION" ;; + guix-dir) guix_dir ;; + set-files) set_files ;; *) sed -n '2,20p' "$0"; exit 2 ;; esac diff --git a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl index 9d6fd23cd..364bb7964 100644 --- a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl +++ b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl @@ -192,6 +192,7 @@ every fix marked *auto* for you. |PV-W32 |a Makefile remains |use the Justfile |no |PV-W33 |Python files remain |not an estate language |no |PV-W34 |npm/yarn/pnpm/TypeScript files remain |use bun |no +|PV-W35 |both guix.scm and build/guix.scm exist |keep one; the engine uses build/ |no |=== Still stuck? `just doctor > doctor.txt 2>&1` and open an issue at From 39b790f5301dc9a0f243a80c3e4c5e9d04a25b53 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:33:33 +0100 Subject: [PATCH 05/20] fix(provisioning): no faked zig/bun tests; one ai-install sentence Found while provisioning rsr-template-repo against the canon: - zig `test` runs only when build.zig declares a "test" step (bench and run already checked); bun's fallback `bun test` runs only when test files exist, because `bun test` exits 0 on none. - ai-setup reads the "Just say it" sentence from the README's [[ai-install]] section, so the README and the recipe cannot disagree. - __GUIX_PREFIX__ slot: SETUP and the dev warm-up name build/manifest.scm etc. when the Guix trio lives under build/; guix.scm templates compute %source-dir from their own location. - hp_app_name falls back to the origin remote's name (worktrees). - provision.just: doc comments on the per-language verbs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../provisioning/PROVISIONING-STANDARD.adoc | 4 ++-- .../templates/build/just/provision-lib.sh | 17 ++++++++++++++--- .../templates/build/just/provision-modes.sh | 8 +++++++- .../templates/build/just/provision.just | 13 +++++++++++-- .../provisioning/templates/docs/SETUP.adoc.tmpl | 10 +++++----- .../templates/guix/guix.scm.cargo.tmpl | 5 ++++- .../templates/guix/guix.scm.source.tmpl | 5 ++++- .../templates/llm-warmup-dev.adoc.tmpl | 4 ++-- 8 files changed, 49 insertions(+), 17 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index dae3eb4b0..4a0fdd4ad 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -79,10 +79,10 @@ of the same name can never shadow them. |`heal` |applies every fix marked *auto*, runs `heal-local`, then `doctor` |`dev-shell` |`guix shell -m /manifest.scm` when Guix is present, otherwise the mise environment |`toolchain-refresh` |`mise up --bump`, re-lock `mise.lock`, re-pin `channels.scm` (the TRUST-DEFAULTS-POLICY recipe) -|`build`, `test`, `bench`, `lint`, `fmt`, `run`, `deps` |the deed's override, else the language default, else an explicit "N/A for this archetype" and exit 0; nothing is faked +|`build`, `test`, `bench`, `lint`, `fmt`, `run`, `deps` |the deed's override, else the language default, else an explicit "N/A for this archetype" and exit 0; nothing is faked. A language default runs only when what it needs exists (a `test` step in `build.zig`, test files for `bun test`, which exits 0 on none) |`eval` |`test` + `bench` with timings, saved under `.eval/` |`config-show` |where the toolchain and configuration are declared -|`ai-setup` |prints the "Just say it" sentence and the guide path +|`ai-setup` |prints the "Just say it" sentence, read from the first listing block of the README's `[[ai-install]]` section so it is written in one place, and the guide path |`ai-warmup ` |prints that warm-up for pasting into any AI |`opsm` |how to fetch this repository with OPSM, and how to get OPSM |`langs`, `search`, `version` |discovery helpers diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index 762ea6f9d..046096bb5 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -215,7 +215,7 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) zd=$(dirname "${zb#./}"); local cdz=""; [ "$zd" != . ] && cdz="cd '$zd' && " case "$v" in build) echo "${cdz}zig build" ;; - test) echo "${cdz}zig build test" ;; + test) grep -qs '"test"' "$zb" && echo "${cdz}zig build test" ;; bench) grep -qs '"bench"' "$zb" && echo "${cdz}zig build bench -Doptimize=ReleaseFast" ;; fmt) echo "zig fmt $zd" ;; lint) echo "zig fmt --check $zd" ;; @@ -250,7 +250,9 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) bun:deps) [ -f bun.lock ] || [ -f bun.lockb ] && echo "bun install --frozen-lockfile" || echo "bun install" ;; bun:build) grep -qs '"build"[[:space:]]*:' package.json && echo "bun run build" ;; - bun:test) grep -qs '"test"[[:space:]]*:' package.json && echo "bun run test" || echo "bun test" ;; + # `bun test` exits 0 when it finds no test files, so it runs only when some exist. + bun:test) if grep -qs '"test"[[:space:]]*:' package.json; then echo "bun run test" + elif git ls-files 2>/dev/null | grep -qE '(\.|_)(test|spec)\.(js|mjs|cjs|jsx)$'; then echo "bun test"; fi ;; bun:bench) grep -qs '"bench"[[:space:]]*:' package.json && echo "bun run bench" ;; bun:lint) grep -qs '"lint"[[:space:]]*:' package.json && echo "bun run lint" ;; bun:fmt) grep -qs '"fmt"[[:space:]]*:' package.json && echo "bun run fmt" ;; @@ -465,8 +467,17 @@ cmd_toolchain_refresh() { echo "Commit the diff above (signed) as: chore(toolchain): weekly refresh" } +# The sentence people are told to say lives in one place a reader sees: the first +# listing block of the README's [[ai-install]] section. The deed's ai-say-it and +# the generic line are fallbacks for a README without that section. just_say_it() { - local line; line=$(deed ai-say-it "") + local line="" r + for r in README.adoc README.md; do + [ -f "$r" ] || continue + line=$(awk '/^\[\[ai-install\]\]/{on=1} on && /^----$/{if (inb) exit; inb=1; next} inb && NF{print; exit}' "$r") + [ -n "$line" ] && break + done + [ -z "$line" ] && line=$(deed ai-say-it "") [ -z "$line" ] && line="Set up $REPO_NAME from https://github.com/$REPO_SLUG — follow docs/AI_INSTALLATION_GUIDE.adoc in that repo." printf '%s' "$line" } diff --git a/3-practice/provisioning/templates/build/just/provision-modes.sh b/3-practice/provisioning/templates/build/just/provision-modes.sh index 96ebb5eaf..f6242cb67 100755 --- a/3-practice/provisioning/templates/build/just/provision-modes.sh +++ b/3-practice/provisioning/templates/build/just/provision-modes.sh @@ -27,7 +27,13 @@ hp__deed() { # $1 key, $2 default — flat (key "value") read from provisioning_ } hp_archetype() { hp__deed archetype "library"; } -hp_app_name() { hp__deed name "$(basename "$REPO_DIR")"; } +# Without a deed, the origin remote names the repo (a worktree or renamed clone +# has another directory name); the directory is the last resort. +hp_app_name() { + local u; u=$(git -C "$REPO_DIR" config --get remote.origin.url 2>/dev/null || true) + u=${u%.git}; u=${u##*/} + hp__deed name "${u:-$(basename "$REPO_DIR")}" +} hp_platform() { case "$(uname -s)" in diff --git a/3-practice/provisioning/templates/build/just/provision.just b/3-practice/provisioning/templates/build/just/provision.just index c7d65f9bb..330be867a 100644 --- a/3-practice/provisioning/templates/build/just/provision.just +++ b/3-practice/provisioning/templates/build/just/provision.just @@ -71,25 +71,34 @@ langs: search pattern: @{{lib}} search {{quote(pattern)}} -# Per-language defaults for the contract verbs (used when the root Justfile -# has no recipe of its own) +# Per-language defaults for the contract verbs, reached as `just provision::` +# or through a root recipe that delegates (`build: provision::build`). A root +# recipe with its own body is the repo-specific override. + +# Build with every detected language's toolchain build: @{{lib}} lang-run build +# Run every detected language's tests test: @{{lib}} lang-run test +# Run every detected language's benchmarks bench: @{{lib}} lang-run bench +# Lint every detected language lint: @{{lib}} lang-run lint +# Format every detected language's sources fmt: @{{lib}} lang-run fmt +# Run the project (N/A for libraries) run: @{{lib}} lang-run run +# Fetch every detected language's dependencies deps: @{{lib}} lang-run deps diff --git a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl index 364bb7964..789a40816 100644 --- a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl +++ b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl @@ -13,7 +13,7 @@ exactly what is written here, so this page is also what to read when they stop. Repository:: https://github.com/__REPO_SLUG__ Kind:: __ARCHETYPE__ (__LANGS__) Toolchain declared in:: `mise.toml` (versions `latest`, made concrete in `mise.lock`), and -`manifest.scm` / `guix.scm` / `channels.scm` for Guix +`__GUIX_PREFIX__manifest.scm` / `__GUIX_PREFIX__guix.scm` / `__GUIX_PREFIX__channels.scm` for Guix Every command below is also a `just` recipe:: run `just` to list them [[prerequisites]] @@ -114,7 +114,7 @@ Anything that does not apply to a __ARCHETYPE__ says so and exits 0; nothing is [source,bash] ---- -just toolchain-refresh # mise up --bump, re-lock mise.lock, re-pin channels.scm +just toolchain-refresh # mise up --bump, re-lock mise.lock, re-pin __GUIX_PREFIX__channels.scm just heal # re-apply safe fixes if something drifted ---- @@ -125,9 +125,9 @@ With GNU Guix installed you can skip mise for everything Guix packages: [source,bash] ---- -guix shell -m manifest.scm # the development shell -guix time-machine -C channels.scm -- shell -m manifest.scm # the exact, pinned Guix -guix build -f guix.scm # the package +guix shell -m __GUIX_PREFIX__manifest.scm # the development shell +guix time-machine -C __GUIX_PREFIX__channels.scm -- shell -m __GUIX_PREFIX__manifest.scm # the exact, pinned Guix +guix build -f __GUIX_PREFIX__guix.scm # the package ---- Guix does not package __GUIX_GAPS__; the Guix shell includes mise, so inside it run diff --git a/3-practice/provisioning/templates/guix/guix.scm.cargo.tmpl b/3-practice/provisioning/templates/guix/guix.scm.cargo.tmpl index 94ef2ef79..29c99984c 100644 --- a/3-practice/provisioning/templates/guix/guix.scm.cargo.tmpl +++ b/3-practice/provisioning/templates/guix/guix.scm.cargo.tmpl @@ -13,7 +13,10 @@ (guix build-system cargo) ((guix licenses) #:prefix license:)) -(define %source-dir (dirname (current-filename))) +;; The repository root: this file sits at the root or in build/ (PROVISIONING-STANDARD §1). +(define %source-dir + (let ((d (dirname (current-filename)))) + (if (string=? (basename d) "build") (dirname d) d))) (load (string-append %source-dir "/build/guix/crates.scm")) ; defines %crate-inputs ;; Build products and caches never enter the source (git-predicate is not used: diff --git a/3-practice/provisioning/templates/guix/guix.scm.source.tmpl b/3-practice/provisioning/templates/guix/guix.scm.source.tmpl index e6e41f856..bfc5433b0 100644 --- a/3-practice/provisioning/templates/guix/guix.scm.source.tmpl +++ b/3-practice/provisioning/templates/guix/guix.scm.source.tmpl @@ -15,7 +15,10 @@ (gnu packages) ((guix licenses) #:prefix license:)) -(define %source-dir (dirname (current-filename))) +;; The repository root: this file sits at the root or in build/ (PROVISIONING-STANDARD §1). +(define %source-dir + (let ((d (dirname (current-filename)))) + (if (string=? (basename d) "build") (dirname d) d))) ;; A spec may name an output ("rust:cargo"); plain specification->package cannot. (define (spec->input spec) (call-with-values (lambda () (specification->package+output spec)) diff --git a/3-practice/provisioning/templates/llm-warmup-dev.adoc.tmpl b/3-practice/provisioning/templates/llm-warmup-dev.adoc.tmpl index f2f8a8985..c85e1187c 100644 --- a/3-practice/provisioning/templates/llm-warmup-dev.adoc.tmpl +++ b/3-practice/provisioning/templates/llm-warmup-dev.adoc.tmpl @@ -14,8 +14,8 @@ __SPEC_ARCHITECTURE__ * Declared in `mise.toml` (specs `latest`), made concrete in `mise.lock`. Never hand-edit versions; `just toolchain-refresh` bumps and re-locks. -* Guix alternative: `manifest.scm` (dev shell), `guix.scm` (package), - `channels.scm` (pinned Guix). `just dev-shell` picks whichever is present. +* Guix alternative: `__GUIX_PREFIX__manifest.scm` (dev shell), `__GUIX_PREFIX__guix.scm` (package), + `__GUIX_PREFIX__channels.scm` (pinned Guix). `just dev-shell` picks whichever is present. * `just` ≥ 1.42. The shared recipes live in `build/just/provision.just` (`mod provision`); root recipes in `Justfile` override them. From eaf3a894a7eb8b034a70c74cbd45a89e5a9491d1 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:49:57 +0100 Subject: [PATCH 06/20] feat(provisioning): fmt-check verb, the check-only twin of fmt fmt-check was aliased to lint, which differs per language (clippy, credo). It is now its own contract verb: cargo fmt --check, zig fmt --check, mix format --check-formatted, gleam format --check, dune build @fmt, or a bun "fmt-check" script; N/A elsewhere. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- 3-practice/provisioning/PROVISIONING-STANDARD.adoc | 2 +- 3-practice/provisioning/provisioning-standard_praxis.deed | 2 +- .../descriptiles/provisioning_praxis.deed.tmpl | 3 ++- 3-practice/provisioning/templates/Justfile.tmpl | 2 +- .../provisioning/templates/build/just/provision-check.sh | 2 +- .../provisioning/templates/build/just/provision-lib.sh | 6 ++++++ 3-practice/provisioning/templates/build/just/provision.just | 4 ++++ 7 files changed, 16 insertions(+), 5 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index 4a0fdd4ad..40678145a 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -79,7 +79,7 @@ of the same name can never shadow them. |`heal` |applies every fix marked *auto*, runs `heal-local`, then `doctor` |`dev-shell` |`guix shell -m /manifest.scm` when Guix is present, otherwise the mise environment |`toolchain-refresh` |`mise up --bump`, re-lock `mise.lock`, re-pin `channels.scm` (the TRUST-DEFAULTS-POLICY recipe) -|`build`, `test`, `bench`, `lint`, `fmt`, `run`, `deps` |the deed's override, else the language default, else an explicit "N/A for this archetype" and exit 0; nothing is faked. A language default runs only when what it needs exists (a `test` step in `build.zig`, test files for `bun test`, which exits 0 on none) +|`build`, `test`, `bench`, `lint`, `fmt`, `fmt-check`, `run`, `deps` |the deed's override, else the language default, else an explicit "N/A for this archetype" and exit 0; nothing is faked. A language default runs only when what it needs exists (a `test` step in `build.zig`, test files for `bun test`, which exits 0 on none) |`eval` |`test` + `bench` with timings, saved under `.eval/` |`config-show` |where the toolchain and configuration are declared |`ai-setup` |prints the "Just say it" sentence, read from the first listing block of the README's `[[ai-install]]` section so it is written in one place, and the guide path diff --git a/3-practice/provisioning/provisioning-standard_praxis.deed b/3-practice/provisioning/provisioning-standard_praxis.deed index c55338741..c55b20fa5 100644 --- a/3-practice/provisioning/provisioning-standard_praxis.deed +++ b/3-practice/provisioning/provisioning-standard_praxis.deed @@ -63,7 +63,7 @@ ;; Launcher modes call the engine directly, so no root recipe can shadow them. (verbs :provisioning ("setup" "doctor" "heal" "dev-shell" "toolchain-refresh") - :lifecycle ("build" "test" "bench" "eval" "lint" "fmt" "run" "deps") + :lifecycle ("build" "test" "bench" "eval" "lint" "fmt" "fmt-check" "run" "deps") :guidance ("ai-setup" "ai-warmup" "config-show" "opsm")) (launcher-modes (mode :flag "--setup" :verb "setup") diff --git a/3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl b/3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl index d10d736ca..542809760 100644 --- a/3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl +++ b/3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl @@ -6,7 +6,7 @@ ;; * one key per line, value in double quotes, and no double quote inside a value ;; (put anything that needs quoting in build/just/setup-local.sh or a script); ;; * an empty value "" means "use the per-language default"; -;; * a verb key (build test bench lint fmt run deps) replaces the default command. +;; * a verb key (build test bench lint fmt fmt-check run deps) replaces the default command. ;; Canon: hyperpolymath/standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc (praxis-deed :schema-version "1.0.0" @@ -20,6 +20,7 @@ :bench "" :lint "" :fmt "" + :fmt-check "" :run "" :deps "" ;; Facts shown by `just config-show` and checked by `just doctor` diff --git a/3-practice/provisioning/templates/Justfile.tmpl b/3-practice/provisioning/templates/Justfile.tmpl index 893b98c01..779138e78 100644 --- a/3-practice/provisioning/templates/Justfile.tmpl +++ b/3-practice/provisioning/templates/Justfile.tmpl @@ -4,7 +4,7 @@ # __APP_NAME__ — every task is a recipe here; `just` lists them. # The provisioning contract (setup doctor heal eval dev-shell toolchain-refresh # ai-setup ai-warmup config-show opsm) comes from build/just/provision.just, and -# the per-language defaults for build/test/bench/lint/fmt/run from the same +# the per-language defaults for build/test/bench/lint/fmt/fmt-check/run from the same # module. Define a recipe here to override one for this repository. # Needs just >= 1.42 (a root recipe depending on a module recipe, e.g. `doctor: provision::doctor`). diff --git a/3-practice/provisioning/templates/build/just/provision-check.sh b/3-practice/provisioning/templates/build/just/provision-check.sh index 7fbb808e3..357ed84ed 100755 --- a/3-practice/provisioning/templates/build/just/provision-check.sh +++ b/3-practice/provisioning/templates/build/just/provision-check.sh @@ -20,7 +20,7 @@ REPO="${1:-.}" cd "$REPO" || { echo "provision-check: no such directory: $REPO" >&2; exit 1; } MIN_JUST="1.42.0" -VERBS="setup doctor heal dev-shell toolchain-refresh ai-setup ai-warmup eval config-show opsm build test bench lint fmt run deps" +VERBS="setup doctor heal dev-shell toolchain-refresh ai-setup ai-warmup eval config-show opsm build test bench lint fmt fmt-check run deps" BANNED="python deno node nodejs npm yarn pnpm go golang java kotlin make black ruff" # Files the provisioning set owns; residue anywhere else is not ours to judge. # The engine scripts under build/just/ are excluded: they name slot syntax in their diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index 046096bb5..bbec60270 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -191,6 +191,7 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) rust:bench) grep -rqs '\[\[bench\]\]\|criterion\|divan' --include=Cargo.toml . && echo "cargo bench" ;; rust:lint) echo "cargo clippy --all-targets -- -D warnings" ;; rust:fmt) echo "cargo fmt --all" ;; + rust:fmt-check) echo "cargo fmt --all -- --check" ;; rust:run) echo "cargo run --release" ;; idris2:*) @@ -218,6 +219,7 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) test) grep -qs '"test"' "$zb" && echo "${cdz}zig build test" ;; bench) grep -qs '"bench"' "$zb" && echo "${cdz}zig build bench -Doptimize=ReleaseFast" ;; fmt) echo "zig fmt $zd" ;; + fmt-check) echo "zig fmt --check $zd" ;; lint) echo "zig fmt --check $zd" ;; run) grep -qs '"run"' "$zb" && echo "${cdz}zig build run" ;; esac ;; @@ -228,12 +230,14 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) elixir:bench) ls bench/*.exs >/dev/null 2>&1 && echo "for f in bench/*.exs; do mix run \"\$f\"; done" ;; elixir:lint) grep -qs ':credo' mix.exs && echo "mix credo --strict" || echo "mix compile --warnings-as-errors" ;; elixir:fmt) echo "mix format" ;; + elixir:fmt-check) echo "mix format --check-formatted" ;; elixir:run) echo "mix run --no-halt" ;; gleam:deps) echo "gleam deps download" ;; gleam:build) echo "gleam build" ;; gleam:test) echo "gleam test" ;; gleam:fmt) echo "gleam format" ;; + gleam:fmt-check) echo "gleam format --check" ;; gleam:lint) echo "gleam format --check" ;; gleam:run) echo "gleam run" ;; @@ -241,6 +245,7 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) ocaml:build) echo "opam exec -- dune build" ;; ocaml:test) echo "opam exec -- dune test" ;; ocaml:fmt) echo "opam exec -- dune fmt" ;; + ocaml:fmt-check) echo "opam exec -- dune build @fmt" ;; haskell:deps) echo "cabal update && cabal build all --only-dependencies" ;; haskell:build) echo "cabal build all" ;; @@ -256,6 +261,7 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) bun:bench) grep -qs '"bench"[[:space:]]*:' package.json && echo "bun run bench" ;; bun:lint) grep -qs '"lint"[[:space:]]*:' package.json && echo "bun run lint" ;; bun:fmt) grep -qs '"fmt"[[:space:]]*:' package.json && echo "bun run fmt" ;; + bun:fmt-check) grep -qs '"fmt-check"[[:space:]]*:' package.json && echo "bun run fmt-check" ;; bun:run) grep -qs '"start"[[:space:]]*:' package.json && echo "bun run start" ;; docs:test) have asciidoctor && echo "for f in \$(git ls-files '*.adoc'); do asciidoctor -o /dev/null --failure-level=WARN \"\$f\" || exit 1; done" ;; diff --git a/3-practice/provisioning/templates/build/just/provision.just b/3-practice/provisioning/templates/build/just/provision.just index 330be867a..f9c3a02c6 100644 --- a/3-practice/provisioning/templates/build/just/provision.just +++ b/3-practice/provisioning/templates/build/just/provision.just @@ -95,6 +95,10 @@ lint: fmt: @{{lib}} lang-run fmt +# Check formatting without rewriting anything (the CI form of fmt) +fmt-check: + @{{lib}} lang-run fmt-check + # Run the project (N/A for libraries) run: @{{lib}} lang-run run From b234caf5a3dd4a9fcbd96817d11825de71a22fbb Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:59:42 +0100 Subject: [PATCH 07/20] =?UTF-8?q?fix(provisioning):=20#1096=20review=20?= =?UTF-8?q?=E2=80=94=20hook=20isolation,=20dispatch=20rc,=20quoting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - provision-lib.sh: build/just/doctor-local.sh runs sourced in a subshell; an `exit` or tripped `set -e` is FAIL PV-E51 and the checks it finished still count (tally path baked into the EXIT trap, since set -e unwinds locals). Tested: normal hook 19/1/0, `exit 3` 19/0/1, `set -e; false` 19/0/1. - provision-lib.sh: recipe tools (trivy) pinned in mise only where a recipe uses them. - provision-modes.sh: hp_provision_or_return replaces `&& exit $?`, which returned success for a failing mode. - provision.just: quote the ai-warmup audience argument. - check-launcher-standard-currency.sh: CURRENT_VERSION 0.5.0. - PV-E51 registered in the deed, the standard and the SETUP troubleshooting table. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../provisioning/PROVISIONING-STANDARD.adoc | 3 ++ .../provisioning-standard_praxis.deed | 3 +- .../templates/build/just/provision-lib.sh | 40 +++++++++++++++++-- .../templates/build/just/provision-modes.sh | 16 ++++++-- .../templates/build/just/provision.just | 2 +- .../templates/docs/SETUP.adoc.tmpl | 1 + scripts/check-launcher-standard-currency.sh | 2 +- 7 files changed, 58 insertions(+), 9 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index 40678145a..59aed7e6f 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -97,6 +97,9 @@ A repository keeps its own checks as root recipes named `doctor-local`, `setup-local` and `heal-local`, or as scripts `build/just/{doctor,setup,heal}-local.sh`. The canon verbs run both. A failing `doctor-local` is a FAIL (PV-E50), so it turns `just doctor`, `./launcher.sh --doctor` and CI red together. +A `build/just/doctor-local.sh` runs sourced in a subshell, so an `exit` or a +tripped `set -e` in it cannot end the doctor early; it is reported as FAIL (PV-E51) +and the checks it did finish still count. The estate's pre-existing boilerplate doctors (the `rsr-diag` and `toolchain-check` families, 326 of 346 measured on 2026-09-30) print `[FAIL]` diff --git a/3-practice/provisioning/provisioning-standard_praxis.deed b/3-practice/provisioning/provisioning-standard_praxis.deed index c55b20fa5..b49f7eda0 100644 --- a/3-practice/provisioning/provisioning-standard_praxis.deed +++ b/3-practice/provisioning/provisioning-standard_praxis.deed @@ -72,6 +72,7 @@ (mode :flag "--ai-setup" :verb "ai-setup")) ;; A repository's own checks: root recipes -local, or scripts ;; build/just/-local.sh. A failing doctor-local is FAIL PV-E50. + ;; A doctor-local.sh that exits (or trips set -e) before its end is FAIL PV-E51. (local-hooks :verbs ("doctor" "setup" "heal") :suffix "-local") (not-applicable :behaviour "print why and exit 0; never fake a result") @@ -121,6 +122,6 @@ (doctor :summary-line "{name}: N PASS, N WARN, N FAIL" :exit "non-zero iff any FAIL" - :fail-codes ("PV-E01" "PV-E02" "PV-E03" "PV-E10" "PV-E11" "PV-E20" "PV-E27" "PV-E40" "PV-E50") + :fail-codes ("PV-E01" "PV-E02" "PV-E03" "PV-E10" "PV-E11" "PV-E20" "PV-E27" "PV-E40" "PV-E50" "PV-E51") :warn-codes ("PV-W01" "PV-W20" "PV-W21" "PV-W22" "PV-W23" "PV-W24" "PV-W25" "PV-W26" "PV-W27" "PV-W28" "PV-W29" "PV-W30" "PV-W31" "PV-W32" "PV-W33" "PV-W34" "PV-W35"))) diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index bbec60270..1f5c3153d 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -153,13 +153,25 @@ guix_specs() { # from pack and haskell from ghcup or Guix: neither is a mise tool. The recipes # themselves need just and shellcheck, so those two are always declared. MISE_BASE="just shellcheck" +# Tools a repo's own recipes call (e.g. `deps-audit` runs trivy): pinned only where used. +RECIPE_TOOLS="trivy" lang_mise() { case "$1" in rust) echo "rust" ;; zig) echo "zig" ;; julia) echo "julia" ;; elixir) echo "erlang elixir" ;; gleam) echo "erlang gleam" ;; ocaml) echo "opam" ;; bun) echo "bun" ;; docs) echo "lychee" ;; esac; } +recipe_tools() { + local t f body="" + for f in Justfile justfile build/just/*.just; do + [ -f "$f" ] && body+=$(grep -v '^[[:space:]]*#' "$f")$'\n' + done + for t in $RECIPE_TOOLS; do + [[ "$body" =~ (^|[^[:alnum:]_-])$t([^[:alnum:]_-]|$) ]] && echo "$t" + done +} + mise_tools() { local l t seen=" " out=() - for t in $MISE_BASE $(for l in "${LANGS[@]}"; do lang_mise "$l"; done); do + for t in $MISE_BASE $(for l in "${LANGS[@]}"; do lang_mise "$l"; done) $(recipe_tools); do case "$seen" in *" $t "*) ;; *) out+=("$t"); seen="$seen$t " ;; esac done printf "%s\n" "${out[*]}" @@ -375,8 +387,30 @@ cmd_doctor() { [ $any -eq 0 ] && pass "no deno / nix / make / python / npm leftovers" local hook=build/just/doctor-local.sh - if [ -f "$hook" ]; then hdr "Repo-specific checks ($hook)"; # shellcheck source=/dev/null - . "$hook"; fi + if [ -f "$hook" ]; then + hdr "Repo-specific checks ($hook)" + # Sourced so the hook can call pass/warn/fail, but in a subshell so an `exit` + # or `set -e` in it cannot end the doctor before the summary. The EXIT trap + # hands the hook's tally back; an early exit is itself a FAIL. + local tally qtally hp w f rc + tally=$(mktemp) + printf -v qtally '%q' "$tally" + # shellcheck disable=SC2030,SC2031 # the subshell's counts return via $tally + ( PASS=0 WARN=0 FAIL=0 hook_done=0 + # The path is baked in now: when `set -e` trips, bash unwinds this + # function's locals before the EXIT trap runs, so $tally is gone by then. + # shellcheck disable=SC2064 + trap "printf '%d %d %d %d\n' \"\$PASS\" \"\$WARN\" \"\$FAIL\" \"\$hook_done\" > $qtally" EXIT + # shellcheck source=/dev/null + . "$hook" + hook_done=1 ) + rc=$? + read -r hp w f hook_done < "$tally" || { hp=0 w=0 f=0 hook_done=0; } + rm -f "$tally" + # shellcheck disable=SC2031 + PASS=$((PASS+hp)) WARN=$((WARN+w)) FAIL=$((FAIL+f)) + [ "$hook_done" = 1 ] || fail "PV-E51 $hook exited (status $rc) before it finished; its later checks did not run" + fi if has_recipe doctor-local; then hdr "Repo-specific checks (just doctor-local)" just doctor-local && pass "doctor-local" || fail "PV-E50 the repo-specific doctor-local recipe failed (output above)" diff --git a/3-practice/provisioning/templates/build/just/provision-modes.sh b/3-practice/provisioning/templates/build/just/provision-modes.sh index f6242cb67..4aa00e08c 100755 --- a/3-practice/provisioning/templates/build/just/provision-modes.sh +++ b/3-practice/provisioning/templates/build/just/provision-modes.sh @@ -9,10 +9,11 @@ # this and calls hp_launcher_main. # Existing app launchers: add ONE line before their mode switch — # -# . "$REPO_DIR/build/just/provision-modes.sh" && hp_provision_dispatch "$@" && exit $? +# . "$REPO_DIR/build/just/provision-modes.sh" && hp_provision_or_return "$@" # -# hp_provision_dispatch returns 99 when the mode is not a provisioning -# mode, so the app's own switch then handles it. +# A provisioning mode runs and the launcher exits with its status (a failed +# --setup exits non-zero); any other mode returns, so the app's own switch +# handles it. hp_provision_dispatch itself returns 99 for "not mine". # # Canon: hyperpolymath/standards launcher/launcher-standard_praxis.deed # (archetypes, provisioning-modes) and @@ -74,6 +75,15 @@ hp_just() { hp_ensure_just || return 1; (cd "$REPO_DIR" && just "$@"); } hp_lib() { (cd "$REPO_DIR" && bash build/just/provision-lib.sh "$@"); } # Returns the mode's exit code, or 99 when "$1" is not a provisioning mode. +# The one-line hook for existing launchers: exit with a provisioning mode's +# status, or return (0) so the caller's own mode switch runs. +hp_provision_or_return() { + hp_provision_dispatch "$@" + local rc=$? + [ "$rc" -eq 99 ] && return 0 + exit "$rc" +} + hp_provision_dispatch() { case "${1:-}" in --setup) hp_lib setup ;; diff --git a/3-practice/provisioning/templates/build/just/provision.just b/3-practice/provisioning/templates/build/just/provision.just index f9c3a02c6..9f1ac7c46 100644 --- a/3-practice/provisioning/templates/build/just/provision.just +++ b/3-practice/provisioning/templates/build/just/provision.just @@ -49,7 +49,7 @@ ai-setup: # Print (and copy) the warm-up for an AI session: user, dev or maintainer ai-warmup who="user": - @{{lib}} ai-warmup {{who}} + @{{lib}} ai-warmup {{quote(who)}} # Run test + bench, time both, and write .eval/.txt eval: diff --git a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl index 789a40816..dc724ed68 100644 --- a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl +++ b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl @@ -176,6 +176,7 @@ every fix marked *auto* for you. |PV-E27 |launcher.sh is not executable |`chmod +x launcher.sh` |yes |PV-E40 |neither Guix nor mise is installed, so there is no dev shell |install mise (step 1) |no |PV-E50 |this repository's own `doctor-local` checks failed |read the output above the summary line; the recipe is in the Justfile |no +|PV-E51 |`build/just/doctor-local.sh` exited before its end (an `exit` or a failing command under `set -e`) |the doctor names the exit status; fix the script so it reports with pass/warn/fail instead of exiting |no |PV-W01 |mise is not installed |install mise (step 1) |no |PV-W20 |mise.lock is missing, so `latest` is not concrete |`just toolchain-refresh` |yes |PV-W21 |both mise.toml and .mise.toml exist |merge into mise.toml, delete .mise.toml |no diff --git a/scripts/check-launcher-standard-currency.sh b/scripts/check-launcher-standard-currency.sh index 7e6ae2a67..7a6482aca 100755 --- a/scripts/check-launcher-standard-currency.sh +++ b/scripts/check-launcher-standard-currency.sh @@ -62,7 +62,7 @@ set -uo pipefail CANONICAL_FILE="launcher-standard_praxis.deed" RETIRED_FILE="launcher-standard.a2ml" -CURRENT_VERSION="0.4.0" +CURRENT_VERSION="0.5.0" SELF_TEST_TMP="" # Path globs exempt as historical records. Matched against the repo-relative From 89c1d32b7a8b4cd54fcb989758ff062520e88c17 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:21:08 +0100 Subject: [PATCH 08/20] fix(provisioning): one set of predicates for doctor and the CI gate Running provision-check.sh on a deliberately weak fixture showed three holes: a 0-byte mise.lock passed (presence was -f), a fake guix.scm passed (the stub test was a blacklist), and doctor and the check used different stub regexes and different banned lists. provision-lib.sh now owns three predicates that both call: mise-banned, mise-lock-gaps (every mise.toml tool needs a concrete version in mise.lock, and the file must carry sha256 checksums) and guix-stub (positive: every package field present, manifest lists specifications, channels pin a 40-hex commit). It also gains fact verbs the generator fills templates from (guix-gaps, tool-table, system-deps), so no second per-language table exists. Mutants killed: empty lock, lock without a tool, versionless block, empty version, aqua:denoland/deno, fake guix.scm, template residue, unpinned channels. Controls pass: a real `mise lock` output, a real guix.scm. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../provisioning/PROVISIONING-STANDARD.adoc | 10 +- .../templates/build/just/provision-check.sh | 19 +-- .../templates/build/just/provision-lib.sh | 138 +++++++++++++++++- .../templates/docs/SETUP.adoc.tmpl | 4 +- .../templates/guix/manifest.scm.tmpl | 2 +- 5 files changed, 146 insertions(+), 27 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index 59aed7e6f..fb23bc153 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -170,9 +170,15 @@ A repository conforms when, on a fresh clone: . `./launcher.sh --help` and `--version` exit 0; . `just --list` parses and lists every verb in §2; -. `mise.toml` names no banned tool, and `mise.lock` exists; -. `guix.scm` is not a stub; +. `mise.toml` names no banned tool, and `mise.lock` gives every `mise.toml` tool a + concrete version and carries `sha256` checksums; +. `guix.scm`, `manifest.scm` and `channels.scm` are not stubs. The test is positive: + `guix.scm` must define every package field (name, version, source, build-system, + home-page, synopsis, description, licence), `manifest.scm` must list + specifications and `channels.scm` must pin a 40-hex commit; . no template residue remains (§5); . `just doctor` exits 0 once `just setup` has run. `templates/build/just/provision-check.sh` checks items 1–5 without network access. +It and doctor ask the same predicates in `provision-lib.sh` (`mise-banned`, +`mise-lock-gaps`, `guix-stub`), so the CI gate and `just doctor` cannot disagree. diff --git a/3-practice/provisioning/templates/build/just/provision-check.sh b/3-practice/provisioning/templates/build/just/provision-check.sh index 357ed84ed..266684221 100755 --- a/3-practice/provisioning/templates/build/just/provision-check.sh +++ b/3-practice/provisioning/templates/build/just/provision-check.sh @@ -21,7 +21,6 @@ cd "$REPO" || { echo "provision-check: no such directory: $REPO" >&2; exit 1; } MIN_JUST="1.42.0" VERBS="setup doctor heal dev-shell toolchain-refresh ai-setup ai-warmup eval config-show opsm build test bench lint fmt fmt-check run deps" -BANNED="python deno node nodejs npm yarn pnpm go golang java kotlin make black ruff" # Files the provisioning set owns; residue anywhere else is not ours to judge. # The engine scripts under build/just/ are excluded: they name slot syntax in their # own comments and code, and realign owns them. README.adoc is scanned only inside @@ -73,23 +72,15 @@ fi echo "[3] mise" if [ ! -f mise.toml ]; then bad "mise.toml missing" else - tools=$(awk '/^\[tools\]/{t=1;next} /^\[/{t=0} t && /=/{sub(/[ \t]*=.*/,""); gsub(/["\x27 ]/,""); print}' mise.toml) - hit="" - for t in $tools; do - base=${t##*:}; base=${base%%@*}; base=${base##*/} # "aqua:denoland/deno" -> deno - for b in $BANNED; do [ "$base" = "$b" ] && hit="$hit$t "; done - done - if [ -n "$hit" ]; then bad "mise.toml names banned tools: $hit"; else ok "mise.toml names no banned tool"; fi + if hit=$(lib mise-banned); then ok "mise.toml names no banned tool"; else bad "mise.toml names banned tools: $hit"; fi [ -f .mise.toml ] && bad "both mise.toml and .mise.toml exist" - if [ -f mise.lock ]; then ok "mise.lock present"; else bad "mise.lock missing (latest is not concrete; run: mise lock)"; fi + if lg=$(lib mise-lock-gaps); then ok "mise.lock pins every mise.toml tool"; else bad "$lg (latest is not concrete; run: mise lock)"; fi fi echo "[4] guix" -gs=$(gp guix.scm) -if [ ! -f "$gs" ]; then bad "guix.scm missing (neither guix.scm nor build/guix.scm)" -elif grep -qE '\{\{|\(inputs \(list\)\)|\(source #f\)' "$gs"; then bad "$gs is a stub" -else ok "$gs is not a stub"; fi -for g in "$(gp manifest.scm)" "$(gp channels.scm)"; do [ -f "$g" ] || bad "$g missing"; done +for g in "$(gp guix.scm)" "$(gp manifest.scm)" "$(gp channels.scm)"; do + if r=$(lib guix-stub "$g"); then ok "$g is not a stub"; else bad "$g: $r"; fi +done echo "[5] template residue" f0=$fails diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index 1f5c3153d..338b6d796 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -15,7 +15,11 @@ # Usage: provision-lib.sh [args] # verbs: langs doctor setup heal dev-shell toolchain-refresh ai-setup # ai-warmup eval config-show opsm lang-run -# search version guix-specs mise-tools +# search version +# facts: guix-specs mise-tools langs guix-dir set-files guix-gaps tool-table +# system-deps +# predicates (print why and exit 1, or exit 0 silently): +# guix-stub FILE, mise-lock-gaps, mise-banned # # Exit: 0 ok | 1 a FAIL was found / a step failed | 2 usage error set -uo pipefail @@ -311,7 +315,116 @@ has_recipe() { have just && just --summary 2>/dev/null | tr " " "\n" | grep -qx version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } # Tools that must never appear in a repo's toolchain (estate language policy). -BANNED_TOOLS='python|deno|node|nodejs|npm|yarn|pnpm|typescript|rescript|make|black|ruff|pip|poetry|nix' +BANNED_TOOLS='python|deno|node|nodejs|npm|yarn|pnpm|typescript|rescript|make|black|ruff|pip|poetry|nix|go|golang|java|kotlin' + +# --------------------------------------------------------------------------- +# Shared predicates. doctor and provision-check.sh both call THESE (the check via +# `provision-lib.sh guix-stub|mise-lock-gaps|mise-banned`): a gate with its own +# copy of a test passes what doctor warns about. + +# The keys of mise.toml [tools], quotes stripped ("cargo:foo" stays cargo:foo). +mise_toml_tools() { + [ -f mise.toml ] || return 0 + awk '/^\[tools\]/{t=1;next} /^\[/{t=0} t && /=/{sub(/[ \t]*=.*/,""); gsub(/["\x27 ]/,""); print}' mise.toml +} +# Banned tools named in mise.toml; a backend prefix does not hide one ("aqua:denoland/deno" is deno). +mise_banned() { + local t base hits="" + for t in $(mise_toml_tools); do + base=${t##*:}; base=${base%%@*}; base=${base##*/} + [[ "$base" =~ ^($BANNED_TOOLS)$ ]] && hits="$hits$t " + done + printf '%s' "${hits% }" +} +# Why mise.lock does not pin mise.toml, or nothing when it does. Presence is not +# enough: a zero-byte lock pins nothing. Every [tools] key needs its [[tools.]] +# entry, and the lock must carry checksums. +mise_lock_gaps() { + [ -f mise.toml ] || return 0 + [ -f mise.lock ] || { echo "mise.lock missing"; return; } + [ -s mise.lock ] || { echo "mise.lock is empty"; return; } + local t miss="" + # A tool is pinned when its [[tools.X]] block carries a concrete version line. + for t in $(mise_toml_tools); do + awk -v a="[[tools.$t]]" -v b="[[tools.\"$t\"]]" ' + $0 == a || $0 == b { inb = 1; next } + /^\[/ { inb = 0 } + inb && /^version = "[^"]+"/ { ok = 1 } + END { exit !ok }' mise.lock || miss="$miss$t " + done + [ -n "$miss" ] && { echo "mise.lock does not pin: ${miss% }"; return; } + grep -q '^checksum = "sha256:' mise.lock || echo "mise.lock carries no checksums" +} +# Why a Guix file is a stub, or nothing when it is real. The test is positive: a +# guix.scm must define every field a package needs, not merely avoid known stub +# shapes; `(package (name "x") (source (local-file ".")))` is a stub. +guix_stub_reason() { + local f=$1 k miss="" + [ -f "$f" ] || { echo "missing"; return; } + grep -qE '\{\{|__[A-Z][A-Z_]*__' "$f" && { echo "unfilled template slots"; return; } + grep -qE '\(inputs \(list\)\)|\(source #f\)' "$f" && { echo "empty inputs or no source"; return; } + case "${f##*/}" in + manifest.scm) grep -q 'specifications->manifest' "$f" || echo "lists no specifications"; return ;; + channels.scm) grep -qE '\(commit "[0-9a-f]{40}"\)' "$f" || echo "pins no commit"; return ;; + esac + for k in name version source build-system home-page synopsis description license; do + grep -qE "\\(${k}[[:space:]]" "$f" || miss="$miss$k " + done + [ -n "$miss" ] && { echo "no package field: ${miss% }"; return; } + if grep -q 'crates\.scm' "$f"; then + grep -qs 'define %crate-inputs' build/guix/crates.scm || echo "build/guix/crates.scm is missing or defines no %crate-inputs" + fi +} + +# --------------------------------------------------------------------------- +# Facts the generator writes into docs/SETUP.adoc and the AI guide. They live here, +# beside lang_tools and lang_remedy, so no second per-language table exists. +lang_title() { case "$1" in + rust) echo Rust ;; idris2) echo Idris2 ;; julia) echo Julia ;; zig) echo Zig ;; elixir) echo Elixir ;; + gleam) echo Gleam ;; ocaml) echo OCaml ;; haskell) echo Haskell ;; bun) echo Bun ;; docs) echo Docs ;; esac; } +# What a language's route needs from the OS: "fedora|debian|macos command|why", or nothing. +lang_sysdeps() { case "$1" in + rust) echo "gcc pkgconf-pkg-config|build-essential pkg-config|xcode-select --install|Rust links through the system C toolchain, and pkg-config finds C libraries" ;; + idris2) echo "chez-scheme gmp-devel|chezscheme libgmp-dev|brew install chezscheme gmp|pack builds Idris2 on top of Chez Scheme and GMP" ;; + elixir|gleam) echo "gcc gcc-c++ make autoconf ncurses-devel openssl-devel|build-essential autoconf m4 libncurses-dev libssl-dev|brew install autoconf openssl@3|mise builds Erlang/OTP from source (allow ~10 min); the make here is the system tool that build uses, not a Makefile in this repository" ;; + ocaml) echo "gcc make patch unzip bubblewrap|build-essential patch unzip bubblewrap|xcode-select --install|opam compiles OCaml and sandboxes its builds with bubblewrap" ;; + haskell) echo "gcc gcc-c++ gmp-devel make ncurses-devel xz perl|build-essential curl libffi-dev libgmp-dev libncurses-dev|xcode-select --install|ghcup installs GHC, which links through the C toolchain and GMP" ;; +esac; } +guix_gaps() { + local l g gaps="" + for l in "${LANGS[@]}"; do g=$(lang_guix_gap "$l"); [ -n "$g" ] && gaps="$gaps${gaps:+, }$g"; done + printf '%s\n' "${gaps:-none}" +} +tool_table() { + local l t base + for l in "${LANGS[@]}"; do + if [ "$l" = docs ]; then echo "|docs |\`lychee\` |mise use lychee@latest"; continue; fi + t=$(lang_tools "$l"); echo "|$l |\`${t// /\`, \`}\` |$(lang_remedy "$l")" + done + base="$MISE_BASE $(recipe_tools | xargs)"; base=$(echo "$base" | xargs) + echo "|(recipes) |\`${base// /\`, \`}\` |mise install, or your OS package manager (e.g. \`dnf install just ShellCheck\`)" +} +# shellcheck disable=SC2016 # the backticks are AsciiDoc literals, not command substitution +system_deps() { # $1 adoc|ai + local l d seen="" fed deb mac why any=0 + [ "$1" = adoc ] && printf '=== System packages\n' + for l in "${LANGS[@]}"; do + d=$(lang_sysdeps "$l"); [ -z "$d" ] && continue + case "$seen" in *"|$d|"*) continue ;; esac; seen="$seen|$d|"; any=1 + IFS='|' read -r fed deb mac why <<<"$d" + if [ "$1" = adoc ]; then + printf '\n%s: %s.\n\n* Fedora: `sudo dnf install %s`\n* Debian/Ubuntu: `sudo apt install %s`\n* macOS: `%s`\n' \ + "$(lang_title "$l")" "$why" "$fed" "$deb" "$mac" + else + printf '* %s needs OS packages (%s): Fedora `sudo dnf install %s` · Debian/Ubuntu `sudo apt install %s` · macOS `%s`.\n' \ + "$(lang_title "$l")" "$why" "$fed" "$deb" "$mac" + fi + done + if [ "$1" = adoc ]; then + if [ $any -eq 1 ]; then printf '\nThe Guix development shell (`%s`) provides these itself, so inside `just dev-shell` none of them is needed.\n' "$(gpath manifest.scm)" + else printf '\nNone beyond git and a shell: every tool this repository needs comes from mise, or from the Guix shell.\n'; fi + elif [ $any -eq 0 ]; then printf '* No other OS packages: every tool comes from mise.\n'; fi +} cmd_doctor() { printf '%s doctor — %s (%s; languages: %s)\n' "$REPO_NAME" "$REPO_SLUG" "$ARCHETYPE" "${LANGS[*]}" @@ -348,17 +461,19 @@ cmd_doctor() { hdr "Repository provisioning files" [ -f mise.toml ] && pass "mise.toml" || fail "PV-E20 mise.toml missing — the toolchain is undeclared" - [ -f mise.lock ] && pass "mise.lock (latest → concrete versions)" || warn "PV-W20 mise.lock missing — run: just toolchain-refresh" + local lg; lg=$(mise_lock_gaps) + [ -z "$lg" ] && pass "mise.lock pins every mise.toml tool (latest → concrete, checksummed)" || warn "PV-W20 $lg — run: just toolchain-refresh" [ -f .mise.toml ] && [ -f mise.toml ] && warn "PV-W21 both mise.toml and .mise.toml — mise merges them; keep only mise.toml" [ -f .tool-versions ] && warn "PV-W22 .tool-versions present — a second toolchain source; fold it into mise.toml" if [ -f mise.toml ]; then - local bad; bad=$(sed -n '/^\[tools\]/,/^\[/p' mise.toml | grep -oE "^[\"']?($BANNED_TOOLS)[\"']?[[:space:]]*=" | tr -d "\"'= " | tr '\n' ' ') - [ -z "$bad" ] && pass "mise.toml pins no banned tool" || warn "PV-W23 mise.toml pins banned tool(s): $bad(language policy: bun, no python/deno/node/make)" + local bad; bad=$(mise_banned) + [ -z "$bad" ] && pass "mise.toml pins no banned tool" || warn "PV-W23 mise.toml pins banned tool(s): $bad (language policy: bun, no python/deno/node/make)" fi # hypatia guix_not_stub reads guix.scm AND build/guix.scm; an unfilled __PLACEHOLDER__ is a stub too. - local g gstub="" - for g in guix.scm build/guix.scm manifest.scm; do - [ -f "$g" ] && grep -qE '\{\{|\(inputs \(list\)\)|\(source #f\)|__[A-Z][A-Z_]*__' "$g" && gstub="$gstub$g " + local g r gstub="" + for g in guix.scm build/guix.scm "$(gpath manifest.scm)" "$(gpath channels.scm)"; do + [ -f "$g" ] || continue + r=$(guix_stub_reason "$g"); [ -n "$r" ] && gstub="$gstub$g ($r) " done local gs gm; gs=$(gpath guix.scm); gm=$(gpath manifest.scm) [ -f guix.scm ] && [ -f build/guix.scm ] && warn "PV-W35 both guix.scm and build/guix.scm exist — two Guix sources; keep one (build/ is used)" @@ -642,5 +757,12 @@ case "${1:-}" in version) echo "provision-lib $PROVISION_LIB_VERSION" ;; guix-dir) guix_dir ;; set-files) set_files ;; + guix-gaps) guix_gaps ;; + tool-table) tool_table ;; + system-deps) shift; system_deps "${1:-adoc}" ;; + # Predicates: print why, exit 1; print nothing, exit 0. + guix-stub) shift; r=$(guix_stub_reason "${1:?usage: guix-stub FILE}"); [ -z "$r" ] || { echo "$r"; exit 1; } ;; + mise-lock-gaps) r=$(mise_lock_gaps); [ -z "$r" ] || { echo "$r"; exit 1; } ;; + mise-banned) r=$(mise_banned); [ -z "$r" ] || { echo "$r"; exit 1; } ;; *) sed -n '2,20p' "$0"; exit 2 ;; esac diff --git a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl index dc724ed68..769a47ae2 100644 --- a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl +++ b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl @@ -130,8 +130,8 @@ guix time-machine -C __GUIX_PREFIX__channels.scm -- shell -m __GUIX_PREFIX__mani guix build -f __GUIX_PREFIX__guix.scm # the package ---- -Guix does not package __GUIX_GAPS__; the Guix shell includes mise, so inside it run -`mise install` for those. `just dev-shell` picks Guix when it is present and mise +Tools Guix does not package: __GUIX_GAPS__. The Guix shell includes mise, so inside +it run `mise install` for those. `just dev-shell` picks Guix when it is present and mise when it is not. [[opsm]] diff --git a/3-practice/provisioning/templates/guix/manifest.scm.tmpl b/3-practice/provisioning/templates/guix/manifest.scm.tmpl index 0795c6a32..f465e7e82 100644 --- a/3-practice/provisioning/templates/guix/manifest.scm.tmpl +++ b/3-practice/provisioning/templates/guix/manifest.scm.tmpl @@ -7,7 +7,7 @@ ;; guix time-machine -C channels.scm -- shell -m manifest.scm # the pinned Guix ;; ;; Minted by provision-set from the languages detected here (__LANGS__). -;; Tools Guix does not package — __GUIX_GAPS__ — come from mise, which this +;; Tools Guix does not package: __GUIX_GAPS__. They come from mise, which this ;; shell provides: guix shell -m manifest.scm -- mise install ;; Canon: hyperpolymath/standards 3-practice/provisioning/PROVISIONING-STANDARD.adoc (specifications->manifest From 93342bf6052a5fa5178d7f4bfa3e883f04fa8456 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:29:23 +0100 Subject: [PATCH 09/20] docs(provisioning): banned-tool mise.toml is replaced; app launchers Realign replaces a mise.toml that names a banned tool (PV-W23), carrying its other [tools] entries, so the deno-to-bun ruling can execute. The template launcher serves library/tool/theory/docs; app launchers come from launch-scaffolder mint and source the same provisioning modes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- 3-practice/provisioning/PROVISIONING-STANDARD.adoc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index fb23bc153..fab92cbaa 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -53,14 +53,19 @@ PV-W35. them. The *generated* `launcher.sh` is rendered from the template and the repo's deed. Realign re-renders it only when its header carries the `@launcher-deed` block, which marks it as generated. A hand-written launcher is kept, and gets -the provisioning modes by sourcing `build/just/provision-modes.sh`. +the provisioning modes by sourcing `build/just/provision-modes.sh`. The template +renders the launcher for `library`, `tool`, `theory` and `docs`; an `app` launcher, +which needs its runtime modes, is rendered by `launch-scaffolder mint` from the +app's own config and sources the same file. A minted set whose deed `:repo` differs from the checkout's own slug was *inherited* (for example from `rsr-template-repo` through GitHub's "Use this template"). Realign treats inherited files as stubs and re-mints them, so a new repository is never born describing its template. *Minted* files are written once, then owned by the repository: realign creates them when missing and replaces them only while they are still stubs -(doctor PV-W24–W28), never once filled. The Justfile is merged: an existing +(doctor PV-W24–W28), never once filled. One exception: a `mise.toml` that names a +banned tool (PV-W23) is replaced too, and its other `[tools]` entries are carried +over, because a repository cannot own a toolchain the estate has banned. The Justfile is merged: an existing custom `doctor`, `setup` or `heal` recipe is renamed `doctor-local`, `setup-local` or `heal-local`, and the canon verbs run it (§3). From 7e6f3db66f5257e30e3a98241ca8055291c7a8a1 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:25:55 +0100 Subject: [PATCH 10/20] feat(provisioning): toolchain-refresh regenerates build/guix/crates.scm guix.scm.cargo.tmpl promised that `just toolchain-refresh` regenerates the crate inputs from Cargo.lock; nothing did. New verb `crates-scm` runs `guix import crate --lockfile` (GUIX may name a container wrapper) and writes build/guix/crates.scm whole or not at all: the output is accepted only when it defines one origin per registry crate in Cargo.lock, since a containerised guix loses its exit status. Otherwise PV-E41 and the old file stays. Verified on launch-scaffolder's Cargo.lock: 151/151 origins, loaded by `guix repl` ((length %crate-inputs) = 151, origin? #t), byte-identical on regeneration. Two mutants killed: truncated importer output (10/151) and a failing importer (0/151) both exit 1 with PV-E41, file sha256 unchanged. channels.scm is reclassified engine -> minted: it is re-pinned per repo, so it is checked for a 40-hex pin, never byte-compared with the canon. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../provisioning/PROVISIONING-STANDARD.adoc | 5 +- .../provisioning-standard_praxis.deed | 2 +- .../templates/build/just/provision-lib.sh | 56 ++++++++++++++++++- .../templates/docs/SETUP.adoc.tmpl | 1 + 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index fab92cbaa..42684424c 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -28,10 +28,11 @@ below, except forks, archived repositories, `007` and the two vaults |`build/just/provision.just` |engine |the `provision::` just module: every verb below |`build/just/provision-lib.sh` |engine |the implementation of every verb (bash only, shellcheck-clean) |`build/just/provision-modes.sh` |engine |the launcher's provisioning dispatch -|`channels.scm` |engine |the pinned Guix commit (placed beside `guix.scm`, see below) +|`channels.scm` |minted |the pinned Guix commit (placed beside `guix.scm`, see below). Copied from the canon at mint, then re-pinned per repository by `toolchain-refresh`, so it is checked for a 40-hex commit pin, never byte-compared with the canon |`Justfile` |merged |`mod provision` plus root delegations; the repo's own recipes are kept |`mise.toml` + `mise.lock` |minted |the toolchain: `latest` in `mise.toml`, made concrete and checksummed in `mise.lock` |`guix.scm`, `manifest.scm` |minted |the Guix package and development shell (at the root, or all three under `build/` where the repository already keeps `build/guix.scm`) +|`build/guix/crates.scm` |generated |Rust repositories: one Guix origin per registry crate in `Cargo.lock`, and `%crate-inputs` for `guix.scm`. Written by `guix import crate --lockfile` through `provision-lib.sh crates-scm`; never hand-edited |`.machine_readable/descriptiles/provisioning_praxis.deed` |minted |this repo's facts: archetype, languages, verb overrides, system deps, ports, config |`docs/SETUP.adoc` |minted |the complete manual route, with the doctor-code troubleshooting table |`docs/AI_INSTALLATION_GUIDE.adoc` |minted |the steps an AI assistant follows @@ -83,7 +84,7 @@ of the same name can never shadow them. |`doctor` |PASS / WARN / FAIL for every requirement, each with a code and its fix; the last line is `: N PASS, N WARN, N FAIL`; exits non-zero if and only if something FAILed |`heal` |applies every fix marked *auto*, runs `heal-local`, then `doctor` |`dev-shell` |`guix shell -m /manifest.scm` when Guix is present, otherwise the mise environment -|`toolchain-refresh` |`mise up --bump`, re-lock `mise.lock`, re-pin `channels.scm` (the TRUST-DEFAULTS-POLICY recipe) +|`toolchain-refresh` |`mise up --bump`, re-lock `mise.lock`, re-pin `channels.scm`, regenerate `build/guix/crates.scm` from `Cargo.lock` when `guix.scm` loads it (the TRUST-DEFAULTS-POLICY recipe). The crate file is replaced only when the importer defined every registry crate; otherwise PV-E41 and the old file stays |`build`, `test`, `bench`, `lint`, `fmt`, `fmt-check`, `run`, `deps` |the deed's override, else the language default, else an explicit "N/A for this archetype" and exit 0; nothing is faked. A language default runs only when what it needs exists (a `test` step in `build.zig`, test files for `bun test`, which exits 0 on none) |`eval` |`test` + `bench` with timings, saved under `.eval/` |`config-show` |where the toolchain and configuration are declared diff --git a/3-practice/provisioning/provisioning-standard_praxis.deed b/3-practice/provisioning/provisioning-standard_praxis.deed index b49f7eda0..7f01e1c12 100644 --- a/3-practice/provisioning/provisioning-standard_praxis.deed +++ b/3-practice/provisioning/provisioning-standard_praxis.deed @@ -122,6 +122,6 @@ (doctor :summary-line "{name}: N PASS, N WARN, N FAIL" :exit "non-zero iff any FAIL" - :fail-codes ("PV-E01" "PV-E02" "PV-E03" "PV-E10" "PV-E11" "PV-E20" "PV-E27" "PV-E40" "PV-E50" "PV-E51") + :fail-codes ("PV-E01" "PV-E02" "PV-E03" "PV-E10" "PV-E11" "PV-E20" "PV-E27" "PV-E40" "PV-E41" "PV-E50" "PV-E51") :warn-codes ("PV-W01" "PV-W20" "PV-W21" "PV-W22" "PV-W23" "PV-W24" "PV-W25" "PV-W26" "PV-W27" "PV-W28" "PV-W29" "PV-W30" "PV-W31" "PV-W32" "PV-W33" "PV-W34" "PV-W35"))) diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index 338b6d796..2d12dd85e 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -13,7 +13,7 @@ # overwrites this file from canon. # # Usage: provision-lib.sh [args] -# verbs: langs doctor setup heal dev-shell toolchain-refresh ai-setup +# verbs: langs doctor setup heal dev-shell toolchain-refresh crates-scm ai-setup # ai-warmup eval config-show opsm lang-run # search version # facts: guix-specs mise-tools langs guix-dir set-files guix-gaps tool-table @@ -605,6 +605,9 @@ cmd_dev_shell() { fi } +# Weekly toolchain refresh: bump mise pins and the lock, re-pin the Guix channel, +# regenerate build/guix/crates.scm when guix.scm loads it, then show the diff +# for a signed commit. cmd_toolchain_refresh() { hdr "mise: bump 'latest' resolutions and re-lock" have mise || { fail "mise not found"; return 1; } @@ -618,10 +621,58 @@ cmd_toolchain_refresh() { guix describe --format=channels > "$ch.new" 2>/dev/null && mv "$ch.new" "$ch" && info "$ch re-pinned to the current guix commit" else info "guix not installed — $ch left as-is (CI re-pins it)"; fi fi - git --no-pager diff --stat -- mise.toml mise.lock "$ch" 2>/dev/null + local cr="" + if [ -f Cargo.lock ] && grep -qs 'crates\.scm' "$(gpath guix.scm)"; then + hdr "guix: crate inputs from Cargo.lock" + cr=build/guix/crates.scm + if have "${GUIX%% *}"; then cmd_crates_scm || return 1 + else info "guix not installed — $cr left as-is (CI regenerates it)"; fi + fi + git --no-pager diff --stat -- mise.toml mise.lock "$ch" $cr 2>/dev/null echo "Commit the diff above (signed) as: chore(toolchain): weekly refresh" } +# The guix command (default: guix), e.g. a wrapper that runs it in a container +# with this directory mounted. +GUIX="${GUIX:-guix}" + +# Write build/guix/crates.scm: every registry crate in Cargo.lock as a Guix +# origin, and %crate-inputs listing them for guix.scm. Written whole or not at +# all. The importer's output is accepted only when it defines exactly one crate +# source per registry package in Cargo.lock: run through a container, guix's exit +# status is lost, so the count is the check. +cmd_crates_scm() { + local dst=build/guix/crates.scm want got tmp spdx + [ -f Cargo.lock ] || { info "no Cargo.lock — no crate inputs"; return 0; } + want=$(grep -c '^source = "registry+' Cargo.lock) + tmp=$(mktemp) || return 1 + if [ "$want" -gt 0 ]; then + # GUIX is split on purpose: it may be a command with arguments. + # shellcheck disable=SC2086 + timeout 1800 $GUIX import crate --lockfile=Cargo.lock "$REPO_NAME" >"$tmp" 2>"$tmp.err" + got=$(grep -c '^(define rust-' "$tmp") + if [ "$got" != "$want" ]; then + fail "PV-E41 guix import crate defined $got of the $want registry crates in Cargo.lock; $dst left as-is: $(grep -m1 -i 'error' "$tmp.err" || grep -v '^+' "$tmp.err" | tail -1)" + rm -f "$tmp" "$tmp.err"; return 1 + fi + fi + # The licence line is guix.scm's own, so the file matches its repository. + spdx=$(grep -m1 'SPDX-License-Identifier' "$(gpath guix.scm)" 2>/dev/null) + mkdir -p build/guix + { + [ -n "$spdx" ] && printf '%s\n' "$spdx" + # shellcheck disable=SC2016 # the backticks are literal text + printf ';; Generated from Cargo.lock by `just toolchain-refresh` (guix import crate\n' + printf ';; --lockfile); never hand-edit it. guix.scm loads it for %%crate-inputs.\n\n' + grep -v '^guix (GNU Guix)' "$tmp" + printf '\n(define %%crate-inputs\n (list' + grep -o '^(define rust-[^ ]*' "$tmp" | awk '{printf "\n %s", $2}' + printf '))\n' + } >"$dst.new" && mv "$dst.new" "$dst" + rm -f "$tmp" "$tmp.err" + info "$dst: $want crate source(s)" +} + # The sentence people are told to say lives in one place a reader sees: the first # listing block of the README's [[ai-install]] section. The deed's ai-say-it and # the generic line are fallbacks for a README without that section. @@ -747,6 +798,7 @@ case "${1:-}" in heal) cmd_heal ;; dev-shell) cmd_dev_shell ;; toolchain-refresh) cmd_toolchain_refresh ;; + crates-scm) cmd_crates_scm ;; ai-setup) cmd_ai_setup ;; ai-warmup) shift; cmd_ai_warmup "${1:-user}" ;; eval) cmd_eval ;; diff --git a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl index 769a47ae2..3825722d2 100644 --- a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl +++ b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl @@ -175,6 +175,7 @@ every fix marked *auto* for you. |PV-E20 |mise.toml is missing |restore it: `git checkout -- mise.toml` |no |PV-E27 |launcher.sh is not executable |`chmod +x launcher.sh` |yes |PV-E40 |neither Guix nor mise is installed, so there is no dev shell |install mise (step 1) |no +|PV-E41 |`just toolchain-refresh` could not regenerate `build/guix/crates.scm`: the Guix crate importer defined fewer crates than `Cargo.lock` lists (often a network failure or a crate not yet on crates.io) |re-run `just toolchain-refresh`; the error is printed after the code. The old file is kept, so the build is unaffected |no |PV-E50 |this repository's own `doctor-local` checks failed |read the output above the summary line; the recipe is in the Justfile |no |PV-E51 |`build/just/doctor-local.sh` exited before its end (an `exit` or a failing command under `set -e`) |the doctor names the exit status; fix the script so it reports with pass/warn/fail instead of exiting |no |PV-W01 |mise is not installed |install mise (step 1) |no From 9b574533d7c53dbcf20c04162ae3c327eae46f10 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:30:55 +0100 Subject: [PATCH 11/20] fix(provisioning): mise.lock checksums are checked per platform table mise_lock_gaps accepted the lock when one `checksum = "sha256:` line existed anywhere, so a tool whose artefacts carried none still passed doctor and provision-check.sh (CodeRabbit, #1096). Every [tools.X."platforms.P"] table now needs its own sha256, and the gap names each tool/platform that lacks one. A tool with no platform tables is not a gap: measured with `mise lock`, core:rust (rustup) and cargo: (built from source) are written with no platform tables and no checksums, while all 97 platform tables across three real locks (just, bun, zig, gleam, shellcheck, erlang, elixir, julia, opam, lychee) carry one. Requiring a checksum per tool would have failed every Rust repository. Controls: the three real locks pass; one zig/linux-x64 checksum removed fails naming exactly that table (the previous code passed it); all removed fails; an empty sha256 value fails. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../templates/build/just/provision-lib.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index 2d12dd85e..db7c3233e 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -353,7 +353,18 @@ mise_lock_gaps() { END { exit !ok }' mise.lock || miss="$miss$t " done [ -n "$miss" ] && { echo "mise.lock does not pin: ${miss% }"; return; } - grep -q '^checksum = "sha256:' mise.lock || echo "mise.lock carries no checksums" + # Checksums are per artefact: every [tools.X."platforms.P"] table needs its own + # sha256, so one checksummed tool cannot vouch for another. A tool with no + # platform tables has no artefact to checksum (core:rust installs through + # rustup, cargo: builds from source), which is what `mise lock` writes for it. + miss=$(awk ' + function close_table() { if (p != "" && !c) printf "%s ", p; p = "" } + /^\[tools\..*platforms\./ { close_table(); p = $0; c = 0 + gsub(/^\[tools\.|\]$|"/, "", p); sub(/\.platforms\./, "/", p); next } + /^\[/ { close_table() } + /^checksum = "sha256:[0-9a-f]+"/ { c = 1 } + END { close_table() }' mise.lock) + [ -z "$miss" ] || echo "mise.lock has no sha256 for: ${miss% }" } # Why a Guix file is a stub, or nothing when it is real. The test is positive: a # guix.scm must define every field a package needs, not merely avoid known stub From 7475b184884339563bed865b288cd4a436d00f6a Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:31:17 +0100 Subject: [PATCH 12/20] docs(provisioning): document the awk helper in mise_lock_gaps Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- 3-practice/provisioning/templates/build/just/provision-lib.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index db7c3233e..fb1d94267 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -358,6 +358,7 @@ mise_lock_gaps() { # platform tables has no artefact to checksum (core:rust installs through # rustup, cargo: builds from source), which is what `mise lock` writes for it. miss=$(awk ' + # Print the open platform table as tool/platform when it carried no sha256. function close_table() { if (p != "" && !c) printf "%s ", p; p = "" } /^\[tools\..*platforms\./ { close_table(); p = $0; c = 0 gsub(/^\[tools\.|\]$|"/, "", p); sub(/\.platforms\./, "/", p); next } From e323e0adda0397899a4e9963abf3124014310227 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:39:00 +0100 Subject: [PATCH 13/20] docs(provisioning): escape the [[ai-install]] anchor in prose asciidoctor processes `[[ai-install]]` as an anchor even inside backticks, so the standard silently defined an `ai-install` id twice (rsr-template-repo#213 failed check-adoc-renders on the same text). `\[[ai-install]]` renders the same literal code and defines nothing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- 3-practice/provisioning/PROVISIONING-STANDARD.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index ed753ee41..8f417a204 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -37,7 +37,7 @@ below, except forks, archived repositories, `007` and the two vaults |`docs/SETUP.adoc` |minted |the complete manual route, with the doctor-code troubleshooting table |`docs/AI_INSTALLATION_GUIDE.adoc` |minted |the steps an AI assistant follows |`llm-warmup-{user,dev,maintainer}.adoc` |minted |paste-in context for any AI, one per audience (at the root, or in `docs/onboarding/` or `docs/` where the repository already keeps them) -|README `[[ai-install]]` section |inserted |"Just say it": one sentence to give any AI +|README `\[[ai-install]]` section |inserted |"Just say it": one sentence to give any AI |=== *Placement.* A file sits at the root only when something needs it there: @@ -88,7 +88,7 @@ of the same name can never shadow them. |`build`, `test`, `bench`, `lint`, `fmt`, `fmt-check`, `run`, `deps` |the deed's override, else the language default, else an explicit "N/A for this archetype" and exit 0; nothing is faked. A language default runs only when what it needs exists (a `test` step in `build.zig`, test files for `bun test`, which exits 0 on none) |`eval` |`test` + `bench` with timings, saved under `.eval/` |`config-show` |where the toolchain and configuration are declared -|`ai-setup` |prints the "Just say it" sentence, read from the first listing block of the README's `[[ai-install]]` section so it is written in one place, and the guide path +|`ai-setup` |prints the "Just say it" sentence, read from the first listing block of the README's `\[[ai-install]]` section so it is written in one place, and the guide path |`ai-warmup ` |prints that warm-up for pasting into any AI |`opsm` |how to fetch this repository with OPSM, and how to get OPSM |`langs`, `search`, `version` |discovery helpers From 42b66c3669bcd3e5816aca313fea20d8ea8fb4db Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:40:48 +0100 Subject: [PATCH 14/20] fix(provisioning): guix-only re-pin, tally-last doctor, artefact kinds - toolchain-refresh re-pins only the `guix` channel's commit in channels.scm (guix_channel_commit / repin_guix_channel) instead of replacing the file with `guix describe` output; when the current commit cannot be read, or the file has no guix channel, it WARNs and leaves the file byte-identical. - doctor prints its "Next:" hint on stderr, so the PASS/WARN/FAIL tally is the last stdout line on every outcome. - The deed marks channels.scm minted (re-pinned per repository, never byte-compared) and lists build/guix/crates.scm as generated; the maintainer warm-up no longer claims realign overwrites launcher.sh or channels.scm. Raised by CodeRabbit on hyperpolymath/launch-scaffolder#67, which vendors this canon. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../provisioning/PROVISIONING-STANDARD.adoc | 2 +- .../provisioning-standard_praxis.deed | 11 +++-- .../templates/build/just/provision-lib.sh | 41 +++++++++++++++++-- .../templates/llm-warmup-maintainer.adoc.tmpl | 15 ++++--- 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc index 8f417a204..8975116e9 100644 --- a/3-practice/provisioning/PROVISIONING-STANDARD.adoc +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -84,7 +84,7 @@ of the same name can never shadow them. |`doctor` |PASS / WARN / FAIL for every requirement, each with a code and its fix; the last line is `: N PASS, N WARN, N FAIL`; exits non-zero if and only if something FAILed |`heal` |applies every fix marked *auto*, runs `heal-local`, then `doctor` |`dev-shell` |`guix shell -m /manifest.scm` when Guix is present, otherwise the mise environment -|`toolchain-refresh` |`mise up --bump`, re-lock `mise.lock`, re-pin `channels.scm`, regenerate `build/guix/crates.scm` from `Cargo.lock` when `guix.scm` loads it (the TRUST-DEFAULTS-POLICY recipe). The crate file is replaced only when the importer defined every registry crate; otherwise PV-E41 and the old file stays +|`toolchain-refresh` |`mise up --bump`, re-lock `mise.lock`, re-pin only the `guix` channel commit in `channels.scm` (left unchanged, with a WARN, when the current commit cannot be read), regenerate `build/guix/crates.scm` from `Cargo.lock` when `guix.scm` loads it (the TRUST-DEFAULTS-POLICY recipe). The crate file is replaced only when the importer defined every registry crate; otherwise PV-E41 and the old file stays |`build`, `test`, `bench`, `lint`, `fmt`, `fmt-check`, `run`, `deps` |the deed's override, else the language default, else an explicit "N/A for this archetype" and exit 0; nothing is faked. A language default runs only when what it needs exists (a `test` step in `build.zig`, test files for `bun test`, which exits 0 on none) |`eval` |`test` + `bench` with timings, saved under `.eval/` |`config-show` |where the toolchain and configuration are declared diff --git a/3-practice/provisioning/provisioning-standard_praxis.deed b/3-practice/provisioning/provisioning-standard_praxis.deed index 7f01e1c12..e348893fb 100644 --- a/3-practice/provisioning/provisioning-standard_praxis.deed +++ b/3-practice/provisioning/provisioning-standard_praxis.deed @@ -25,9 +25,11 @@ ;; ------------------------------------------------------------ artefact set ;; engine = identical estate-wide; `provision-set realign` overwrites it. - ;; generated = rendered from the template + deed; realign re-renders it only - ;; when it carries the @launcher-deed block (hand-written ones are - ;; kept and source build/just/provision-modes.sh). + ;; generated = produced, never hand-edited. launcher.sh is rendered from the + ;; template + deed and re-rendered by realign only when it carries + ;; the @launcher-deed block (hand-written ones are kept and source + ;; build/just/provision-modes.sh); build/guix/crates.scm is + ;; written from Cargo.lock by mint and toolchain-refresh. ;; minted = written once, then owned by the repository; realign creates it ;; when missing and replaces it only while it is still a stub, or ;; when the deed's :repo is another repository's (inherited). @@ -38,7 +40,8 @@ (file :path "build/just/provision-lib.sh" :kind engine :mode "100755") (file :path "build/just/provision-modes.sh" :kind engine :mode "100755") (file :path "build/just/provision-check.sh" :kind engine :mode "100755") - (file :path "channels.scm" :kind engine) + (file :path "channels.scm" :kind minted) ; re-pinned by toolchain-refresh + (file :path "build/guix/crates.scm" :kind generated) ; from Cargo.lock, when guix.scm loads it (file :path "Justfile" :kind merged) (file :path "mise.toml" :kind minted) (file :path "mise.lock" :kind minted) diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index fb1d94267..a76ef33ae 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -438,6 +438,9 @@ system_deps() { # $1 adoc|ai elif [ $any -eq 0 ]; then printf '* No other OS packages: every tool comes from mise.\n'; fi } +# Diagnose the provisioning set and toolchain, then the repository's own +# doctor-local checks. The PASS/WARN/FAIL tally is the last line of stdout; +# returns non-zero when anything FAILed. cmd_doctor() { printf '%s doctor — %s (%s; languages: %s)\n' "$REPO_NAME" "$REPO_SLUG" "$ARCHETYPE" "${LANGS[*]}" @@ -545,7 +548,8 @@ cmd_doctor() { printf '\n%s: %s%d PASS%s, %s%d WARN%s, %s%d FAIL%s\n' "$REPO_NAME" "$G" "$PASS" "$Z" "$Y" "$WARN" "$Z" "$R" "$FAIL" "$Z" if [ "$FAIL" -gt 0 ]; then - echo "Next: 'just heal' fixes what is safe to fix automatically; each FAIL code is explained in docs/SETUP.adoc §Troubleshooting." + # stderr, so the tally above stays the last line of stdout on every outcome. + echo "Next: 'just heal' fixes what is safe to fix automatically; each FAIL code is explained in docs/SETUP.adoc §Troubleshooting." >&2 return 1 fi return 0 @@ -617,6 +621,30 @@ cmd_dev_shell() { fi } +# Print the 40-hex commit of the `guix` channel in a channels list read from +# stdin (`guix describe --format=channels`), or nothing when there is none. +guix_channel_commit() { + awk '/\(name .guix\)/ { g = 1 } + g && match($0, /\(commit "[0-9a-f]{40}"\)/) { print substr($0, RSTART + 9, 40); exit }' +} + +# Re-pin only the `guix` channel's commit in channels file $1 to $2, keeping +# every other line (comments, other channels, introduction) as it is. Returns +# non-zero, leaving the file untouched, when it has no guix channel commit. +repin_guix_channel() { + local ch=$1 pin=$2 tmp + tmp=$(mktemp) || return 1 + if awk -v pin="$pin" ' + /\(name .guix\)/ { g = 1 } + g && !done && sub(/\(commit "[0-9a-f]{40}"\)/, "(commit \"" pin "\")") { done = 1 } + { print } + END { exit !done }' "$ch" > "$tmp"; then + cat "$tmp" > "$ch"; rm -f "$tmp" + else + rm -f "$tmp"; return 1 + fi +} + # Weekly toolchain refresh: bump mise pins and the lock, re-pin the Guix channel, # regenerate build/guix/crates.scm when guix.scm loads it, then show the diff # for a signed commit. @@ -629,8 +657,15 @@ cmd_toolchain_refresh() { if [ -f "$ch" ]; then hdr "guix: channel pin" if have guix; then - guix pull --channels="$ch" --dry-run >/dev/null 2>&1 || true - guix describe --format=channels > "$ch.new" 2>/dev/null && mv "$ch.new" "$ch" && info "$ch re-pinned to the current guix commit" + local pin + pin=$(guix describe --format=channels 2>/dev/null | guix_channel_commit) + if [ -z "$pin" ]; then + warn "could not read the current guix channel commit — $ch left unchanged" + elif repin_guix_channel "$ch" "$pin"; then + info "$ch: guix channel re-pinned to $pin" + else + warn "$ch has no (name 'guix) channel with a commit — left unchanged" + fi else info "guix not installed — $ch left as-is (CI re-pins it)"; fi fi local cr="" diff --git a/3-practice/provisioning/templates/llm-warmup-maintainer.adoc.tmpl b/3-practice/provisioning/templates/llm-warmup-maintainer.adoc.tmpl index 62085cbbb..4295ea3fa 100644 --- a/3-practice/provisioning/templates/llm-warmup-maintainer.adoc.tmpl +++ b/3-practice/provisioning/templates/llm-warmup-maintainer.adoc.tmpl @@ -10,15 +10,18 @@ __APP_NAME__. Read `llm-warmup-dev.adoc` first; this adds what a maintainer owns This repository is provisioned from the estate canon (https://github.com/hyperpolymath/standards, `3-practice/provisioning/`). Repo-specific facts live in `.machine_readable/descriptiles/provisioning_praxis.deed`. -`provision-set realign` overwrites only the engine: `launcher.sh`, -`build/just/provision*.{just,sh}` and `channels.scm`; change behaviour through the -deed, never by hand-editing those. Everything else (`guix.scm` once real, -`manifest.scm`, `mise.toml`, the Justfile, `docs/` and these warm-ups) is minted -once and then belongs to this repository: keep it true as the code changes. +`provision-set realign` overwrites only the engine, `build/just/provision*`; +change behaviour through the deed, never by hand-editing those. It re-renders +`launcher.sh` only when that file carries the `@launcher-deed` block. +`build/guix/crates.scm` is generated from `Cargo.lock`: regenerate it, never +edit it. Everything else (`channels.scm`, re-pinned by `toolchain-refresh`; +`guix.scm` once real, `manifest.scm`, `mise.toml`, the Justfile, `docs/` and +these warm-ups) is minted once and then belongs to this repository: keep it +true as the code changes. == Keeping it current -* `just toolchain-refresh` — `mise up --bump`, `mise lock`, re-pin `channels.scm`. +* `just toolchain-refresh` — `mise up --bump`, `mise lock`, re-pin the guix commit in `channels.scm`. Commit the lock changes on their own. * `just doctor` must be clean before a release; WARN codes are tracked, not ignored. * The `provisioning-check` workflow reports drift from the canon. From b01a2451313a2b37ff3519908dcab52bfa393d8a Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:01:39 +0100 Subject: [PATCH 15/20] fix(provisioning): PV-W23 reads every mise config and backend mise merges .tool-versions, mise.toml and .mise.toml, so a banned tool pinned in either secondary file passed PV-W23, which read mise.toml only. The check now reads all three, flags npm:/pipx:/pip:/go: backends whatever the package name, and bans denojs (the 07-18 sweep's name for deno). The deed's :banned-tools is now the same list as BANNED_TOOLS. Controls: python in .tool-versions and npm:prettier in .mise.toml -> mise-banned rc=1 naming both; the same configs without them -> rc=0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../provisioning-standard_praxis.deed | 9 +++++- .../templates/build/just/provision-lib.sh | 31 ++++++++++++++----- .../templates/docs/SETUP.adoc.tmpl | 2 +- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/3-practice/provisioning/provisioning-standard_praxis.deed b/3-practice/provisioning/provisioning-standard_praxis.deed index e348893fb..14a2e5a85 100644 --- a/3-practice/provisioning/provisioning-standard_praxis.deed +++ b/3-practice/provisioning/provisioning-standard_praxis.deed @@ -92,7 +92,14 @@ :mise-lockfile #t :mise-base ("just" "shellcheck") :js-runtime "bun" - :banned-tools ("python" "deno" "node" "npm" "yarn" "pnpm" "go" "java" "kotlin" "make") + ;; One list with BANNED_TOOLS in build/just/provision-lib.sh (launch-scaffolder + ;; tests that they agree). A tool installed through an npm:, pipx:, pip: or go: + ;; backend is banned whatever its name; mise.toml, .mise.toml and + ;; .tool-versions are all read (doctor PV-W23). + :banned-tools ("python" "deno" "denojs" "node" "nodejs" "npm" "yarn" "pnpm" + "typescript" "rescript" "make" "black" "ruff" "pip" "poetry" + "nix" "go" "golang" "java" "kotlin") + :banned-backends ("npm" "pipx" "pip" "go") ;; Measured 2026-09-30: a root recipe depending on a module recipe fails on ;; just 1.31, 1.36, 1.40, 1.41 and works from 1.42. :just-floor "1.42.0" diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index a76ef33ae..26f2d5b71 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -315,24 +315,39 @@ has_recipe() { have just && just --summary 2>/dev/null | tr " " "\n" | grep -qx version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } # Tools that must never appear in a repo's toolchain (estate language policy). -BANNED_TOOLS='python|deno|node|nodejs|npm|yarn|pnpm|typescript|rescript|make|black|ruff|pip|poetry|nix|go|golang|java|kotlin' +BANNED_TOOLS='python|deno|denojs|node|nodejs|npm|yarn|pnpm|typescript|rescript|make|black|ruff|pip|poetry|nix|go|golang|java|kotlin' +BANNED_BACKENDS='npm|pipx|pip|go' # --------------------------------------------------------------------------- # Shared predicates. doctor and provision-check.sh both call THESE (the check via # `provision-lib.sh guix-stub|mise-lock-gaps|mise-banned`): a gate with its own # copy of a test passes what doctor warns about. -# The keys of mise.toml [tools], quotes stripped ("cargo:foo" stays cargo:foo). +# The tools every repository mise config declares, quotes stripped ("cargo:foo" +# stays cargo:foo): the [tools] keys of mise.toml and .mise.toml and the first +# word of each .tool-versions line. mise merges all three, so a predicate that +# read mise.toml alone would pass a banned tool pinned in the others. mise_toml_tools() { - [ -f mise.toml ] || return 0 - awk '/^\[tools\]/{t=1;next} /^\[/{t=0} t && /=/{sub(/[ \t]*=.*/,""); gsub(/["\x27 ]/,""); print}' mise.toml + local f + for f in .tool-versions mise.toml .mise.toml; do + [ -f "$f" ] || continue + if [ "$f" = .tool-versions ]; then + awk '!/^[ \t]*(#|$)/{print $1}' "$f" + else + awk '/^\[tools\]/{t=1;next} /^\[/{t=0} t && /=/{sub(/[ \t]*=.*/,""); gsub(/["\x27 ]/,""); print}' "$f" + fi + done | awk '!seen[$0]++' } -# Banned tools named in mise.toml; a backend prefix does not hide one ("aqua:denoland/deno" is deno). +# Banned tools named in the mise configs. A backend prefix does not hide one +# ("aqua:denoland/deno" is deno), and an npm:/pipx:/pip:/go: backend installs +# through a banned runtime whatever the package is ("npm:prettier"). mise_banned() { local t base hits="" for t in $(mise_toml_tools); do base=${t##*:}; base=${base%%@*}; base=${base##*/} - [[ "$base" =~ ^($BANNED_TOOLS)$ ]] && hits="$hits$t " + if [[ "$base" =~ ^($BANNED_TOOLS)$ ]] || [[ "$t" =~ ^($BANNED_BACKENDS): ]]; then + hits="$hits$t " + fi done printf '%s' "${hits% }" } @@ -480,9 +495,9 @@ cmd_doctor() { [ -z "$lg" ] && pass "mise.lock pins every mise.toml tool (latest → concrete, checksummed)" || warn "PV-W20 $lg — run: just toolchain-refresh" [ -f .mise.toml ] && [ -f mise.toml ] && warn "PV-W21 both mise.toml and .mise.toml — mise merges them; keep only mise.toml" [ -f .tool-versions ] && warn "PV-W22 .tool-versions present — a second toolchain source; fold it into mise.toml" - if [ -f mise.toml ]; then + if [ -f mise.toml ] || [ -f .mise.toml ] || [ -f .tool-versions ]; then local bad; bad=$(mise_banned) - [ -z "$bad" ] && pass "mise.toml pins no banned tool" || warn "PV-W23 mise.toml pins banned tool(s): $bad (language policy: bun, no python/deno/node/make)" + [ -z "$bad" ] && pass "the mise configs pin no banned tool" || warn "PV-W23 a mise config pins banned tool(s): $bad (language policy: bun, no python/deno/node/npm/make)" fi # hypatia guix_not_stub reads guix.scm AND build/guix.scm; an unfilled __PLACEHOLDER__ is a stub too. local g r gstub="" diff --git a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl index 3825722d2..71de1d1e7 100644 --- a/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl +++ b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl @@ -182,7 +182,7 @@ every fix marked *auto* for you. |PV-W20 |mise.lock is missing, so `latest` is not concrete |`just toolchain-refresh` |yes |PV-W21 |both mise.toml and .mise.toml exist |merge into mise.toml, delete .mise.toml |no |PV-W22 |a .tool-versions file competes with mise.toml |fold it into mise.toml |no -|PV-W23 |mise.toml pins a tool the estate does not use |remove that line |no +|PV-W23 |a mise config (mise.toml, .mise.toml, .tool-versions) pins a tool the estate does not use, or an npm:/pipx:/pip:/go: backend |remove that line |no |PV-W24 |a Guix file is an unfilled template |regenerate it (maintainers: `provision-set realign`) |no |PV-W25 |guix.scm is missing |as PV-W24 |no |PV-W26 |manifest.scm is missing |as PV-W24; mise still works |no From bf7c97abfc7240e9c4275c85153e74b50de10eaf Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:05:31 +0100 Subject: [PATCH 16/20] fix(provisioning): a bare mise name with only npm backends is banned prettier = "latest" resolves to npm:prettier, the only backend the mise registry has for it, so it installed through node while passing PV-W23. mise_banned now asks `mise registry` (offline) and flags a bare name whose every backend is npm:/pipx:/pip:/go:. A name with any other backend (shfmt: aqua) or one mise does not know is not flagged, and without mise on PATH nothing is called banned on a guess. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy --- .../templates/build/just/provision-lib.sh | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index 26f2d5b71..96f401222 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -338,15 +338,31 @@ mise_toml_tools() { fi done | awk '!seen[$0]++' } +# True when a bare registry name resolves only to banned backends ("prettier" +# is only npm:prettier). Needs no network: `mise registry` reads the registry +# baked into mise. False when mise is absent or does not know the name, so an +# unknown tool is left to mise-lock to report, never called banned on a guess. +only_banned_backends() { + local b n=0 + command -v mise >/dev/null 2>&1 || return 1 + for b in $(mise registry "$1" 2>/dev/null); do + n=$((n + 1)) + [[ "$b" =~ ^($BANNED_BACKENDS): ]] || return 1 + done + [ "$n" -gt 0 ] +} # Banned tools named in the mise configs. A backend prefix does not hide one -# ("aqua:denoland/deno" is deno), and an npm:/pipx:/pip:/go: backend installs -# through a banned runtime whatever the package is ("npm:prettier"). +# ("aqua:denoland/deno" is deno), an npm:/pipx:/pip:/go: backend installs +# through a banned runtime whatever the package is ("npm:prettier"), and so does +# a bare name with no other backend ("prettier"). mise_banned() { local t base hits="" for t in $(mise_toml_tools); do base=${t##*:}; base=${base%%@*}; base=${base##*/} if [[ "$base" =~ ^($BANNED_TOOLS)$ ]] || [[ "$t" =~ ^($BANNED_BACKENDS): ]]; then hits="$hits$t " + elif [[ "$t" != *:* ]] && only_banned_backends "$t"; then + hits="$hits$t " fi done printf '%s' "${hits% }" From a00fcf3128dcadf0081f821e185a7a12d668e3c3 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:26:20 +0100 Subject: [PATCH 17/20] Update 3-practice/provisioning/templates/launcher.sh.tmpl Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- 3-practice/provisioning/templates/launcher.sh.tmpl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/3-practice/provisioning/templates/launcher.sh.tmpl b/3-practice/provisioning/templates/launcher.sh.tmpl index 32e0243c8..520fcffd6 100755 --- a/3-practice/provisioning/templates/launcher.sh.tmpl +++ b/3-practice/provisioning/templates/launcher.sh.tmpl @@ -24,8 +24,9 @@ # # The launcher for a library / tool / theory / docs repository # (launcher-standard 0.6.0, archetype profile). Every mode is implemented in -# build/just/provision-modes.sh, and every provisioning mode delegates to the -# Justfile. This file is minted by `provision-set`; do not hand-edit it. +# build/just/provision-modes.sh, and every provisioning mode calls the engine +# (build/just/provision-lib.sh) directly. This file is minted by `provision-set`; +# do not hand-edit it. # Repo-specific facts belong in .machine_readable/descriptiles/provisioning_praxis.deed. set -uo pipefail From 71cad01fd63dbf57102d40194449d5831b2878b2 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:29:35 +0000 Subject: [PATCH 18/20] docs(provisioning): document shell template functions --- .../templates/build/just/provision-check.sh | 8 +++++ .../templates/build/just/provision-lib.sh | 36 +++++++++++++++++++ .../templates/build/just/provision-modes.sh | 8 +++++ 3 files changed, 52 insertions(+) diff --git a/3-practice/provisioning/templates/build/just/provision-check.sh b/3-practice/provisioning/templates/build/just/provision-check.sh index 266684221..0be28c482 100755 --- a/3-practice/provisioning/templates/build/just/provision-check.sh +++ b/3-practice/provisioning/templates/build/just/provision-check.sh @@ -28,16 +28,22 @@ VERBS="setup doctor heal dev-shell toolchain-refresh ai-setup ai-warmup eval con # Their locations come from provision-lib.sh (guix-dir, set-files): the engine and # this check must never disagree about where the Guix files or warm-ups live. [ -f "$LIB" ] || { echo "provision-check: engine missing: $LIB" >&2; exit 1; } +# Run the provisioning engine with the checked repository as its root. lib() { PROVISION_ROOT="$PWD" bash "$LIB" "$@"; } GDIR=$(lib guix-dir) SET_FILES=$(lib set-files) +# Print the path to Guix filename $1 using the resolved Guix directory. gp() { [ "$GDIR" = . ] && echo "$1" || echo "$GDIR/$1"; } fails=0 warns=0 +# Print a successful conformance check. ok() { printf ' ok %s\n' "$*"; } +# Print a failed conformance check and increment the failure count. bad() { printf ' FAIL %s\n' "$*"; fails=$((fails + 1)); } +# Print a conformance warning and increment the warning count. warn() { printf ' warn %s\n' "$*"; warns=$((warns + 1)); } +# Return success when version $1 is at least $2, using version sort order. version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } echo "[1] launcher" @@ -84,6 +90,8 @@ done echo "[5] template residue" f0=$fails +# Read text from stdin and report unfilled slots under label $1; +# repository-specific slots are warnings in --dev mode and failures otherwise. residue() { # $1 label; stdin = the text to judge local text mech spec text=$(cat) diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index 96f401222..cb5cf48c9 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -35,11 +35,17 @@ T="timeout 15" # some --version probes hang (observed 2026-09-30); never pr if [ -t 1 ]; then R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[34m'; Z=$'\033[0m'; else R=; G=; Y=; B=; Z=; fi PASS=0; WARN=0; FAIL=0 +# Increment the PASS tally and print the diagnostic message. pass() { PASS=$((PASS+1)); printf ' %sPASS%s %s\n' "$G" "$Z" "$*"; } +# Increment the WARN tally and print the diagnostic message. warn() { WARN=$((WARN+1)); printf ' %sWARN%s %s\n' "$Y" "$Z" "$*"; } +# Increment the FAIL tally and print the diagnostic message. fail() { FAIL=$((FAIL+1)); printf ' %sFAIL%s %s\n' "$R" "$Z" "$*"; } +# Print an informational message without changing diagnostic tallies. info() { printf ' %sinfo%s %s\n' "$B" "$Z" "$*"; } +# Print a section heading for provisioning output. hdr() { printf '\n%s== %s ==%s\n' "$B" "$*" "$Z"; } +# Return success when command $1 is available in the current shell. have() { command -v "$1" >/dev/null 2>&1; } # Where this repository keeps each part of the set. A file sits at the root only @@ -49,6 +55,7 @@ have() { command -v "$1" >/dev/null 2>&1; } # This is the ONE answer: provision-check.sh asks it through `guix-dir` and # `set-files` rather than keeping its own copy. guix_dir() { if [ -f build/guix.scm ]; then echo build; else echo .; fi; } +# Print the first directory containing warm-up guides, falling back to the root. warmup_dir() { local d for d in . docs/onboarding docs; do @@ -56,8 +63,11 @@ warmup_dir() { done echo . } +# Print the path to Guix filename $1 in the repository layout. gpath() { local d; d=$(guix_dir); [ "$d" = . ] && echo "$1" || echo "$d/$1"; } +# Print the warm-up guide path for audience $1 in the repository layout. wpath() { local d; d=$(warmup_dir); [ "$d" = . ] && echo "llm-warmup-$1.adoc" || echo "$d/llm-warmup-$1.adoc"; } +# List the provisioning-owned paths checked for unfilled template slots. set_files() { printf '%s\n' launcher.sh Justfile justfile mise.toml \ "$(gpath guix.scm)" "$(gpath manifest.scm)" "$(gpath channels.scm)" \ @@ -73,6 +83,7 @@ deed() { # $1 key, $2 default [ -z "$v" ] && v=$(grep -oE ":$1[[:space:]]+\"[^\"]*\"" "$DEED" 2>/dev/null | head -1 | sed -E 's/^[^"]*"//; s/"$//') printf '%s' "${v:-$2}" } +# Derive the repository slug from origin, falling back to hyperpolymath/. repo_slug() { local u; u=$(git config --get remote.origin.url 2>/dev/null || true) u=${u%.git}; u=${u#*github.com[:/]} @@ -89,6 +100,7 @@ ARCHETYPE="$(deed archetype "")" # 74 repos have their only build.zig at that depth, measured 2026-09-30). # Order matters only for display. `docs` is reported when nothing else is. first() { find . -maxdepth "${2:-2}" -not -path './.git/*' -not -path '*/node_modules/*' -not -path './target/*' -name "$1" -print -quit 2>/dev/null; } +# Print detected languages, one per line; use docs when no language marker exists. detect_langs() { local out=() [ -n "$(first Cargo.toml)" ] && out+=(rust) @@ -128,6 +140,7 @@ lang_tools() { # docker.io/metacall/guix). Guix has NO idris2, gleam, bun or lychee, and its # julia is 1.8.5: those come from mise, which Guix itself ships ("mise"). GUIX_BASE="git bash coreutils nss-certs just mise shellcheck" +# Print the Guix package specifications for language $1, if available. lang_guix() { case "$1" in rust) echo "rust rust:cargo gcc-toolchain pkg-config" ;; @@ -147,6 +160,7 @@ lang_guix_gap() { idris2) echo "idris2 (via pack)" ;; julia) echo julia ;; gleam) echo gleam ;; bun) echo bun ;; docs) echo lychee ;; esac } +# Print deduplicated base and detected-language Guix package specifications. guix_specs() { local l s=" $GUIX_BASE " for l in "${LANGS[@]}"; do for p in $(lang_guix "$l"); do case "$s" in *" $p "*) ;; *) s="$s$p ";; esac; done; done @@ -159,10 +173,12 @@ guix_specs() { MISE_BASE="just shellcheck" # Tools a repo's own recipes call (e.g. `deps-audit` runs trivy): pinned only where used. RECIPE_TOOLS="trivy" +# Print the mise tool names for language $1, if supported. lang_mise() { case "$1" in rust) echo "rust" ;; zig) echo "zig" ;; julia) echo "julia" ;; elixir) echo "erlang elixir" ;; gleam) echo "erlang gleam" ;; ocaml) echo "opam" ;; bun) echo "bun" ;; docs) echo "lychee" ;; esac; } +# Print known recipe tools referenced outside comments in the repository Justfiles. recipe_tools() { local t f body="" for f in Justfile justfile build/just/*.just; do @@ -173,6 +189,7 @@ recipe_tools() { done } +# Print deduplicated mise tools needed by the base, detected languages and recipes. mise_tools() { local l t seen=" " out=() for t in $MISE_BASE $(for l in "${LANGS[@]}"; do lang_mise "$l"; done) $(recipe_tools); do @@ -181,6 +198,7 @@ mise_tools() { printf "%s\n" "${out[*]}" } +# Print installation guidance for the toolchain of language $1. lang_remedy() { case "$1" in rust) echo "mise use rust@latest (or rustup: https://rustup.rs)" ;; @@ -312,6 +330,7 @@ lang_run() { # $1 verb # CI all see the same result. has_recipe() { have just && just --summary 2>/dev/null | tr " " "\n" | grep -qx "$1"; } +# Return success when version $1 is at least $2, using version sort order. version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } # Tools that must never appear in a repo's toolchain (estate language policy). @@ -433,11 +452,13 @@ lang_sysdeps() { case "$1" in ocaml) echo "gcc make patch unzip bubblewrap|build-essential patch unzip bubblewrap|xcode-select --install|opam compiles OCaml and sandboxes its builds with bubblewrap" ;; haskell) echo "gcc gcc-c++ gmp-devel make ncurses-devel xz perl|build-essential curl libffi-dev libgmp-dev libncurses-dev|xcode-select --install|ghcup installs GHC, which links through the C toolchain and GMP" ;; esac; } +# Print a comma-separated list of tools Guix cannot supply, or none. guix_gaps() { local l g gaps="" for l in "${LANGS[@]}"; do g=$(lang_guix_gap "$l"); [ -n "$g" ] && gaps="$gaps${gaps:+, }$g"; done printf '%s\n' "${gaps:-none}" } +# Print AsciiDoc table rows describing required tools and installation commands. tool_table() { local l t base for l in "${LANGS[@]}"; do @@ -448,6 +469,7 @@ tool_table() { echo "|(recipes) |\`${base// /\`, \`}\` |mise install, or your OS package manager (e.g. \`dnf install just ShellCheck\`)" } # shellcheck disable=SC2016 # the backticks are AsciiDoc literals, not command substitution +# Print OS dependency guidance for detected languages; $1 selects adoc or ai output. system_deps() { # $1 adoc|ai local l d seen="" fed deb mac why any=0 [ "$1" = adoc ] && printf '=== System packages\n' @@ -586,6 +608,8 @@ cmd_doctor() { return 0 } +# Install available toolchains and dependencies, run local setup hooks, then doctor; +# return non-zero when a tracked installation step or verification fails. cmd_setup() { printf '%s setup — installing everything this repository needs\n' "$REPO_NAME" local rc=0 @@ -618,6 +642,7 @@ cmd_setup() { return $rc } +# Apply automatic environment repairs and local heal hooks, then return doctor status. cmd_heal() { printf '%s heal — applying safe, reversible fixes, then re-checking\n' "$REPO_NAME" hdr "Fixes" @@ -639,6 +664,7 @@ cmd_heal() { cmd_doctor } +# Replace this process with the Guix shell or mise environment; fail if neither works. cmd_dev_shell() { local gm; gm=$(gpath manifest.scm) if have guix && [ -f "$gm" ] && ! grep -q '{{' "$gm"; then @@ -765,6 +791,8 @@ just_say_it() { [ -z "$line" ] && line="Set up $REPO_NAME from https://github.com/$REPO_SLUG — follow docs/AI_INSTALLATION_GUIDE.adoc in that repo." printf '%s' "$line" } +# Copy stdin to an available clipboard with a three-second timeout; +# silently consume input and return 1 when no clipboard command is available. clip() { # best-effort; silent when no clipboard exists (CI, SSH) # wl-copy and xclip fork a daemon that inherits stdout; left attached, it # holds any pipeline open forever (observed 2026-09-30). Detach and bound it. @@ -776,6 +804,7 @@ clip() { # best-effort; silent when no clipboard exists (CI, SSH) else cat >/dev/null; return 1; fi timeout 3 "${c[@]}" >/dev/null 2>&1 } +# Print AI-assisted setup instructions and attempt to copy the setup sentence. cmd_ai_setup() { cat <" >&2; return 2 ;; esac @@ -805,6 +836,8 @@ cmd_ai_warmup() { fail "llm-warmup-$who.adoc not found"; return 1 } +# Run test and bench recipes, saving output and timings under .eval/; +# report N/A for skipped work and return 1 if either recipe fails. cmd_eval() { local logf rc=0 s e v r o st logf=".eval/$(date -u +%Y%m%dT%H%M%SZ).txt" @@ -825,6 +858,7 @@ cmd_eval() { return $rc } +# Print resolved repository metadata and provisioning configuration paths. cmd_config_show() { echo "repo: $REPO_SLUG" echo "archetype: $ARCHETYPE" @@ -838,6 +872,7 @@ cmd_config_show() { echo "lib: provision-lib $PROVISION_LIB_VERSION" } +# Print repository-specific OPSM installation instructions. cmd_opsm() { cat <} if have agrep; then git ls-files -z | xargs -0 agrep -n -1 -- "$p" 2>/dev/null diff --git a/3-practice/provisioning/templates/build/just/provision-modes.sh b/3-practice/provisioning/templates/build/just/provision-modes.sh index 35e603363..4e30ce1a4 100755 --- a/3-practice/provisioning/templates/build/just/provision-modes.sh +++ b/3-practice/provisioning/templates/build/just/provision-modes.sh @@ -21,12 +21,14 @@ HP_PROVISION_MODES_VERSION="0.6.0" +# Print the flat deed value for key $1, falling back to default $2. hp__deed() { # $1 key, $2 default — flat (key "value") read from provisioning_praxis.deed local f="$REPO_DIR/.machine_readable/descriptiles/provisioning_praxis.deed" v="" [ -f "$f" ] && v=$(grep -oE "[(:]$1[[:space:]]+\"[^\"]*\"" "$f" | head -1 | sed -E 's/^[^"]*"//; s/"$//') printf '%s' "${v:-$2}" } +# Print the declared repository archetype, defaulting to library. hp_archetype() { hp__deed archetype "library"; } # Without a deed, the origin remote names the repo (a worktree or renamed clone # has another directory name); the directory is the last resort. @@ -36,6 +38,7 @@ hp_app_name() { hp__deed name "${u:-$(basename "$REPO_DIR")}" } +# Print linux, macos, windows or unknown based on the host kernel name. hp_platform() { case "$(uname -s)" in Linux*) echo linux ;; @@ -68,6 +71,7 @@ EOF return 1 } +# Ensure just is available, then run it with the supplied arguments in REPO_DIR. hp_just() { hp_ensure_just || return 1; (cd "$REPO_DIR" && just "$@"); } # The provisioning modes call the engine directly, not a `just` recipe: a repo may # define its own root `doctor`/`setup`/`heal`, and the launcher must still run the @@ -84,6 +88,8 @@ hp_provision_or_return() { exit "$rc" } +# Run the provisioning mode named by $1 and return its status; +# return 99 when the mode is not handled here. hp_provision_dispatch() { case "${1:-}" in --setup) hp_lib setup ;; @@ -94,6 +100,7 @@ hp_provision_dispatch() { esac } +# Print the launcher name, version, commit, platform and architecture. hp_version_line() { local sha ver sha=$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown) @@ -102,6 +109,7 @@ hp_version_line() { printf '%s-launcher %s (%s) [%s-%s]\n' "$(hp_app_name)" "${ver#v}" "$sha" "$(hp_platform)" "$(uname -m)" } +# Print launcher usage and the modes applicable to the repository archetype. hp_help() { local name arch; name=$(hp_app_name); arch=$(hp_archetype) cat < Date: Thu, 1 Oct 2026 14:34:15 +0000 Subject: [PATCH 19/20] docs(provisioning): clarify template function behavior and exit statuses --- .../templates/build/just/provision-check.sh | 1 + .../templates/build/just/provision-lib.sh | 46 +++++++++++++++---- .../templates/build/just/provision-modes.sh | 7 ++- .../templates/guix/guix.scm.source.tmpl | 1 + 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/3-practice/provisioning/templates/build/just/provision-check.sh b/3-practice/provisioning/templates/build/just/provision-check.sh index 0be28c482..496865e43 100755 --- a/3-practice/provisioning/templates/build/just/provision-check.sh +++ b/3-practice/provisioning/templates/build/just/provision-check.sh @@ -92,6 +92,7 @@ echo "[5] template residue" f0=$fails # Read text from stdin and report unfilled slots under label $1; # repository-specific slots are warnings in --dev mode and failures otherwise. +# Mechanical slots always fail. Callers use the tallies, not the return status. residue() { # $1 label; stdin = the text to judge local text mech spec text=$(cat) diff --git a/3-practice/provisioning/templates/build/just/provision-lib.sh b/3-practice/provisioning/templates/build/just/provision-lib.sh index cb5cf48c9..0fcd7d96b 100755 --- a/3-practice/provisioning/templates/build/just/provision-lib.sh +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -77,6 +77,7 @@ set_files() { # --------------------------------------------------------------------------- # Descriptor: flat `:key "value"` reads from provisioning_praxis.deed (s-expression). +# Use default $2 when the value is absent or empty; print without a trailing newline. deed() { # $1 key, $2 default local v="" [ -f "$DEED" ] && v=$(grep -oE "\(:?$1[[:space:]]+\"[^\"]*\"" "$DEED" 2>/dev/null | head -1 | sed -E 's/^[^"]*"//; s/"$//') @@ -99,6 +100,8 @@ ARCHETYPE="$(deed archetype "")" # Zig to three (ffi/zig/build.zig, and rsr-template-repo's src/interface/ffi/build.zig: # 74 repos have their only build.zig at that depth, measured 2026-09-30). # Order matters only for display. `docs` is reported when nothing else is. +# Print the first path matching glob $1 within find depth $2 (default 2), +# excluding .git, node_modules and root target contents; no match prints nothing. first() { find . -maxdepth "${2:-2}" -not -path './.git/*' -not -path '*/node_modules/*' -not -path './target/*' -name "$1" -print -quit 2>/dev/null; } # Print detected languages, one per line; use docs when no language marker exists. detect_langs() { @@ -305,6 +308,8 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) # Run a contract verb across every detected language. N/A is reported, not # faked green: a verb with no command for any language exits 0 but SAYS so. +# A deed override runs alone and its status is returned. Otherwise all available +# language commands run; return the last failing status, or 0 if none failed. lang_run() { # $1 verb local verb=$1 ran=0 rc=0 l cmd override override=$(deed "$verb" "") @@ -374,6 +379,8 @@ only_banned_backends() { # ("aqua:denoland/deno" is deno), an npm:/pipx:/pip:/go: backend installs # through a banned runtime whatever the package is ("npm:prettier"), and so does # a bare name with no other backend ("prettier"). +# Print space-separated matches without a trailing newline; callers determine +# failure from non-empty output. mise_banned() { local t base hits="" for t in $(mise_toml_tools); do @@ -386,9 +393,10 @@ mise_banned() { done printf '%s' "${hits% }" } -# Why mise.lock does not pin mise.toml, or nothing when it does. Presence is not -# enough: a zero-byte lock pins nothing. Every [tools] key needs its [[tools.]] -# entry, and the lock must carry checksums. +# Print the first category of lockfile gaps, or nothing if none is found. +# Skip checking when mise.toml is absent; otherwise check tools from all three +# config files for a non-empty version and platform tables for sha256 entries. +# Callers inspect stdout; version concreteness and checksum contents are not validated. mise_lock_gaps() { [ -f mise.toml ] || return 0 [ -f mise.lock ] || { echo "mise.lock missing"; return; } @@ -417,9 +425,10 @@ mise_lock_gaps() { END { close_table() }' mise.lock) [ -z "$miss" ] || echo "mise.lock has no sha256 for: ${miss% }" } -# Why a Guix file is a stub, or nothing when it is real. The test is positive: a -# guix.scm must define every field a package needs, not merely avoid known stub -# shapes; `(package (name "x") (source (local-file ".")))` is a stub. +# Print why Guix file $1 appears to be a stub, or nothing if the textual checks +# pass. Check required package fields as well as known stub shapes; this does +# not evaluate Scheme or verify that the package builds. Callers inspect stdout. +# For example, `(package (name "x") (source (local-file ".")))` is a stub. guix_stub_reason() { local f=$1 k miss="" [ -f "$f" ] || { echo "missing"; return; } @@ -494,6 +503,8 @@ system_deps() { # $1 adoc|ai # Diagnose the provisioning set and toolchain, then the repository's own # doctor-local checks. The PASS/WARN/FAIL tally is the last line of stdout; # returns non-zero when anything FAILed. +# Warnings alone do not fail. An early doctor-local.sh exit or failed +# doctor-local recipe becomes a FAIL rather than propagating its exit status. cmd_doctor() { printf '%s doctor — %s (%s; languages: %s)\n' "$REPO_NAME" "$REPO_SLUG" "$ARCHETYPE" "${LANGS[*]}" @@ -610,6 +621,8 @@ cmd_doctor() { # Install available toolchains and dependencies, run local setup hooks, then doctor; # return non-zero when a tracked installation step or verification fails. +# May trust mise configuration, extend PATH, initialise opam and make the +# launcher executable. Installation and hook failures do not skip verification. cmd_setup() { printf '%s setup — installing everything this repository needs\n' "$REPO_NAME" local rc=0 @@ -643,6 +656,7 @@ cmd_setup() { } # Apply automatic environment repairs and local heal hooks, then return doctor status. +# Earlier repair and hook failures are not propagated independently of doctor. cmd_heal() { printf '%s heal — applying safe, reversible fixes, then re-checking\n' "$REPO_NAME" hdr "Fixes" @@ -664,7 +678,9 @@ cmd_heal() { cmd_doctor } -# Replace this process with the Guix shell or mise environment; fail if neither works. +# Replace this process with Guix when its manifest exists and has no {{ residue; +# otherwise use mise and SHELL (default bash). Return 1 if neither route is +# available; a failed exec terminates the script without trying the other route. cmd_dev_shell() { local gm; gm=$(gpath manifest.scm) if have guix && [ -f "$gm" ] && ! grep -q '{{' "$gm"; then @@ -705,6 +721,9 @@ repin_guix_channel() { # Weekly toolchain refresh: bump mise pins and the lock, re-pin the Guix channel, # regenerate build/guix/crates.scm when guix.scm loads it, then show the diff # for a signed commit. +# Return 1 for missing mise or failed mise update, locking or crate regeneration. +# Channel pin failures are warnings; unavailable Guix operations are skipped. +# Earlier updates are not rolled back. cmd_toolchain_refresh() { hdr "mise: bump 'latest' resolutions and re-lock" have mise || { fail "mise not found"; return 1; } @@ -742,9 +761,11 @@ GUIX="${GUIX:-guix}" # Write build/guix/crates.scm: every registry crate in Cargo.lock as a Guix # origin, and %crate-inputs listing them for guix.scm. Written whole or not at -# all. The importer's output is accepted only when it defines exactly one crate -# source per registry package in Cargo.lock: run through a container, guix's exit -# status is lost, so the count is the check. +# all. Accept importer output when its rust- definition count matches the number +# of registry packages in Cargo.lock; the importer's exit status is not checked. +# GUIX may contain a command and arguments; import is limited to 1,800 seconds. +# Missing Cargo.lock is a successful no-op. Return 1 for temporary-file creation +# failure or a count mismatch; later write errors are not reliably propagated. cmd_crates_scm() { local dst=build/guix/crates.scm want got tmp spdx [ -f Cargo.lock ] || { info "no Cargo.lock — no crate inputs"; return 0; } @@ -793,6 +814,7 @@ just_say_it() { } # Copy stdin to an available clipboard with a three-second timeout; # silently consume input and return 1 when no clipboard command is available. +# Otherwise return the timeout command's status; suppress clipboard output and errors. clip() { # best-effort; silent when no clipboard exists (CI, SSH) # wl-copy and xclip fork a daemon that inherits stdout; left attached, it # holds any pipeline open forever (observed 2026-09-30). Detach and bound it. @@ -805,6 +827,7 @@ clip() { # best-effort; silent when no clipboard exists (CI, SSH) timeout 3 "${c[@]}" >/dev/null 2>&1 } # Print AI-assisted setup instructions and attempt to copy the setup sentence. +# Clipboard failure is ignored. cmd_ai_setup() { cat <" >&2; return 2 ;; esac @@ -897,6 +921,8 @@ EOF } # Search tracked files for pattern $1 with agrep, falling back to git grep. +# agrep allows one error; git grep is case-insensitive. Return the search +# pipeline or git grep status; an empty or missing pattern terminates the script. cmd_search() { local p=${1:?usage: just search } if have agrep; then git ls-files -z | xargs -0 agrep -n -1 -- "$p" 2>/dev/null diff --git a/3-practice/provisioning/templates/build/just/provision-modes.sh b/3-practice/provisioning/templates/build/just/provision-modes.sh index 4e30ce1a4..eb9efa2fe 100755 --- a/3-practice/provisioning/templates/build/just/provision-modes.sh +++ b/3-practice/provisioning/templates/build/just/provision-modes.sh @@ -49,6 +49,8 @@ hp_platform() { } # Make sure `just` is runnable: PATH, then mise, then say exactly what to do. +# May install just globally via mise, extend PATH or define a just wrapper. +# Return 0 when available, otherwise 1 after printing installation guidance. hp_ensure_just() { command -v just >/dev/null 2>&1 && return 0 if command -v mise >/dev/null 2>&1; then @@ -78,7 +80,6 @@ hp_just() { hp_ensure_just || return 1; (cd "$REPO_DIR" && just "$@"); } # canon (which then runs that repo's *-local recipes). Only needs bash. hp_lib() { (cd "$REPO_DIR" && bash build/just/provision-lib.sh "$@"); } -# Returns the mode's exit code, or 99 when "$1" is not a provisioning mode. # The one-line hook for existing launchers: exit with a provisioning mode's # status, or return (0) so the caller's own mode switch runs. hp_provision_or_return() { @@ -142,7 +143,9 @@ Detected platform: $(hp_platform) EOF } -# Whole-launcher main for non-app archetypes. +# Whole-launcher main for non-app archetypes; use --help when $1 is absent. +# Return the provisioning status, 2 for unknown modes, or 1 for an app runtime +# request. Accepted runtime modes for other archetypes explain N/A and return 0. hp_launcher_main() { local mode=${1:---help} rc case "$mode" in diff --git a/3-practice/provisioning/templates/guix/guix.scm.source.tmpl b/3-practice/provisioning/templates/guix/guix.scm.source.tmpl index bfc5433b0..4555fe518 100644 --- a/3-practice/provisioning/templates/guix/guix.scm.source.tmpl +++ b/3-practice/provisioning/templates/guix/guix.scm.source.tmpl @@ -20,6 +20,7 @@ (let ((d (dirname (current-filename)))) (if (string=? (basename d) "build") (dirname d) d))) ;; A spec may name an output ("rust:cargo"); plain specification->package cannot. +;; Return the package for its default "out" output, otherwise (package output). (define (spec->input spec) (call-with-values (lambda () (specification->package+output spec)) (lambda (pkg out) (if (string=? out "out") pkg (list pkg out))))) From 0281f58d8e67f290aac555aafa23df0c5293f3e0 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:32:01 +0100 Subject: [PATCH 20/20] ci(provisioning): add the provisioning-check reusable gate (#1106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Stacked on #1096.** The base branch is `feat/provisioning-canon`. After #1096 squash-merges, run: ``` git rebase --onto main feat/provisioning-canon feat/provisioning-check-reusable ``` and retarget this PR to `main`. Until then the diff is just this gate. ## What `.github/workflows/provisioning-check-reusable.yml`, the CI gate from `3-practice/provisioning`. It checks the caller against the canon at the workflow's own commit (`job.workflow_sha`), in two steps that report separately: | Step | Fails when | |---|---| | Engine files match the canon | any `build/just/{provision.just,provision-lib.sh,provision-modes.sh,provision-check.sh}` is missing or differs byte-for-byte | | Provisioning set conforms | the **canon** `provision-check.sh`, run without `--dev`, reports FAIL. It loads `provision-lib.sh` from its own directory, so a drifted caller copy cannot vouch for itself | - `channels.scm` is deliberately not compared byte-for-byte: `toolchain-refresh` re-pins it per repository. `provision-check.sh` checks its pin instead. - `just` 1.56.0 comes from the release tarball, pinned by sha256 (the same pin as launch-scaffolder#67). No new `uses:` is added. - `actions.lock` gains the section by hand (checkout only). `canon.lock` lists the reusable as `provisioning` under `[canon.workflows]`. ## Evidence (local dry run of both steps; the CI proof follows on a throwaway caller) | Case | cmp step | provision-check | |---|---|---| | rsr-template-repo #213 head (control) | pass | pass | | mutant: `fmt-check` recipe removed | pass | **FAIL** | | mutant: `python` added to `mise.toml` | pass | **FAIL** (banned tool + unpinned) | | mutant: `provision-lib.sh` changed | **FAIL** | pass | | mutant reverted | pass | pass | The third mutant is why there are two steps: an engine edit that leaves conformance intact is caught only by the byte comparison. ## Known, not new - actionlint does not know the `job.workflow_sha` context. It reports the same thing 4 times on `allowlist-preflight-reusable.yml`. - `gh actions-lock` gives this file the same `sha-as-ref` advisory that every SHA-pinned workflow here carries (94 on the base, 95 with this one). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UkSmyapDUmuGyyZSJmvbKy Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- .../workflows/provisioning-check-reusable.yml | 119 ++++++++++++++++++ canon.lock | 1 + 2 files changed, 120 insertions(+) create mode 100644 .github/workflows/provisioning-check-reusable.yml diff --git a/.github/workflows/provisioning-check-reusable.yml b/.github/workflows/provisioning-check-reusable.yml new file mode 100644 index 000000000..ea04c7048 --- /dev/null +++ b/.github/workflows/provisioning-check-reusable.yml @@ -0,0 +1,119 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# Reusable provisioning-set conformance gate (3-practice/provisioning §8). +# +# Two checks, each reported on its own so one cannot hide the other: +# +# engine drift the caller's build/just/{provision.just,provision-lib.sh, +# provision-modes.sh,provision-check.sh} must be byte-identical +# to the canon at THIS workflow's own commit (job.workflow_sha). +# channels.scm is deliberately not compared: toolchain-refresh +# re-pins it per repository, so a byte compare would go red +# after every refresh. provision-check.sh checks its pin instead. +# conformance the CANON provision-check.sh (not the caller's copy, which may +# have drifted) run against the caller, without --dev: template +# residue of either tier fails, as at the pre-PR gate. +# +# Offline: nothing here builds the project or touches the network beyond the +# two checkouts and the just release tarball, pinned by sha256. +# +# Caller: +# jobs: +# provisioning: +# uses: hyperpolymath/standards/.github/workflows/provisioning-check-reusable.yml@ +name: Provisioning Check Reusable + +on: + workflow_call: + +permissions: + contents: read + +jobs: + provisioning: + name: Provisioning set conforms + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout caller repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ github.repository }} + ref: ${{ github.sha }} + + - name: Checkout the provisioning canon + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/standards + ref: ${{ job.workflow_sha }} + path: .standards-checkout + sparse-checkout: | + 3-practice/provisioning/templates/build/just + sparse-checkout-cone-mode: false + + - name: Stage the canon engine outside the checked tree + shell: bash + run: | + mkdir -p "$RUNNER_TEMP/canon" + cp .standards-checkout/3-practice/provisioning/templates/build/just/* "$RUNNER_TEMP/canon/" + rm -rf .standards-checkout + ls "$RUNNER_TEMP/canon" + + - name: Install just (the engine's runner; >= 1.42 is required) + shell: bash + env: + JUST_VERSION: "1.56.0" + JUST_SHA256: fa2a8ec1015d9df5330941ade12437488fc40d33f9c9f8cd4eb70a26de11b639 + run: | + set +e + tgz="$RUNNER_TEMP/just.tar.gz" + curl -fsSL -o "$tgz" \ + "https://github.com/casey/just/releases/download/${JUST_VERSION}/just-${JUST_VERSION}-x86_64-unknown-linux-musl.tar.gz" \ + && echo "${JUST_SHA256} $tgz" | sha256sum -c - \ + && mkdir -p "$RUNNER_TEMP/bin" \ + && tar -xzf "$tgz" -C "$RUNNER_TEMP/bin" just + STATUS=$? + if [ "$STATUS" -ne 0 ]; then + echo "::error title=just install::could not fetch or verify just ${JUST_VERSION}" + exit "$STATUS" + fi + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + "$RUNNER_TEMP/bin/just" --version + + - name: Engine files match the canon + if: ${{ !cancelled() }} + shell: bash + run: | + set +e + drift=0 + for f in provision.just provision-lib.sh provision-modes.sh provision-check.sh; do + if [ ! -f "build/just/$f" ]; then + echo "::error file=build/just/$f,title=engine missing::build/just/$f is missing (run: launch-scaffolder provision-set realign)" + drift=1 + elif ! cmp -s "build/just/$f" "$RUNNER_TEMP/canon/$f"; then + echo "::error file=build/just/$f,title=engine drift::build/just/$f differs from the canon at standards@${{ job.workflow_sha }} (run: launch-scaffolder provision-set realign)" + drift=1 + else + echo "ok build/just/$f" + fi + done + exit "$drift" + + - name: Provisioning set conforms (provision-check.sh) + if: ${{ !cancelled() }} + shell: bash + run: | + set +e + bash "$RUNNER_TEMP/canon/provision-check.sh" . | tee "$RUNNER_TEMP/check.log" + STATUS="${PIPESTATUS[0]}" + grep '^ FAIL' "$RUNNER_TEMP/check.log" | sed 's/^ FAIL //' | while IFS= read -r line; do + echo "::error title=provisioning::$line" + done + { + echo "## Provisioning check" + echo "" + echo '```' + cat "$RUNNER_TEMP/check.log" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + exit "$STATUS" diff --git a/canon.lock b/canon.lock index e660a70ce..cc4bd129c 100644 --- a/canon.lock +++ b/canon.lock @@ -319,6 +319,7 @@ secrets = "hyperpolymath/standards/.github/workflows/secret-scanner-reusable. mirror = "hyperpolymath/standards/.github/workflows/mirror-reusable.yml" changelog = "hyperpolymath/standards/.github/workflows/changelog-reusable.yml" readme = "hyperpolymath/standards/.github/workflows/readme-derive-reusable.yml" +provisioning = "hyperpolymath/standards/.github/workflows/provisioning-check-reusable.yml" # --------------------------------------------------------------------------- # THE ROLES. One owner per concern; a change belongs at its owning layer