diff --git a/3-practice/provisioning/PROVISIONING-STANDARD.adoc b/3-practice/provisioning/PROVISIONING-STANDARD.adoc new file mode 100644 index 00000000..8975116e --- /dev/null +++ b/3-practice/provisioning/PROVISIONING-STANDARD.adoc @@ -0,0 +1,190 @@ +// 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.6.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` |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 +|`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 +|`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` +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 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. 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). + +== 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 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 +|`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 +|=== + +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. +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]` +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` 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/provisioning-standard_praxis.deed b/3-practice/provisioning/provisioning-standard_praxis.deed new file mode 100644 index 00000000..14a2e5a8 --- /dev/null +++ b/3-practice/provisioning/provisioning-standard_praxis.deed @@ -0,0 +1,137 @@ +;; 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. + ;; 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). + ;; merged = the repository's own content is kept; the canon is added. + (artefacts + (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") + (file :path "build/just/provision-check.sh" :kind engine :mode "100755") + (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) + (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) + ;; 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 + ;; 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" "fmt-check" "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. + ;; 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") + + (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" + ;; 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" + :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-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/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl b/3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl new file mode 100644 index 00000000..54280976 --- /dev/null +++ b/3-practice/provisioning/templates/.machine_readable/descriptiles/provisioning_praxis.deed.tmpl @@ -0,0 +1,32 @@ +;; 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 fmt-check 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 "" + :fmt-check "" + :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 00000000..779138e7 --- /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/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`). + +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 00000000..ddc6d1bf --- /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 00000000..496865e4 --- /dev/null +++ b/3-practice/provisioning/templates/build/just/provision-check.sh @@ -0,0 +1,121 @@ +#!/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 + +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +LIB="$HERE/provision-lib.sh" +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 fmt-check run deps" +# 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. +# 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" +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 + 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 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" +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 +# 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) + # 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 00000000..0fcd7d96 --- /dev/null +++ b/3-practice/provisioning/templates/build/just/provision-lib.sh @@ -0,0 +1,960 @@ +#!/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 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 +# 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 + +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 +# 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 +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 +# 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; } +# Print the first directory containing warm-up guides, falling back to the root. +warmup_dir() { + local d + for d in . docs/onboarding docs; do + compgen -G "$d/llm-warmup-*.adoc" >/dev/null && { echo "$d"; return; } + 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)" \ + "$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). +# 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/"$//') + [ -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[:/]} + [ -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). 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. +# 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() { + local out=() + [ -n "$(first Cargo.toml)" ] && out+=(rust) + [ -n "$(first '*.ipkg' 3)" ] && out+=(idris2) + [ -n "$(first Project.toml 1)" ] && out+=(julia) + [ -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) + { [ -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" +# Print the Guix package specifications for language $1, if available. +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 +} +# 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 + 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" +# 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 + [ -f "$f" ] && body+=$(grep -v '^[[:space:]]*#' "$f")$'\n' + done + for t in $RECIPE_TOOLS; do + [[ "$body" =~ (^|[^[:alnum:]_-])$t([^[:alnum:]_-]|$) ]] && echo "$t" + 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 + case "$seen" in *" $t "*) ;; *) out+=("$t"); seen="$seen$t " ;; esac + done + 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)" ;; + 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: just dev-shell, which uses Guix)" ;; + 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:fmt-check) echo "cargo fmt --all -- --check" ;; + rust:run) echo "cargo run --release" ;; + + idris2:*) + 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" ;; + 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.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); [ -z "$zb" ] && zb=$(first build.zig 4) + zd=$(dirname "${zb#./}"); local cdz=""; [ "$zd" != . ] && cdz="cd '$zd' && " + case "$v" in + build) echo "${cdz}zig build" ;; + 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 ;; + + 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: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" ;; + + 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" ;; + 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" ;; + 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` 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" ;; + 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" ;; + 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. +# 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" "") + 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"; } + +# 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). +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 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() { + 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]++' +} +# 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), 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 + 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% }" +} +# 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; } + [ -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; } + # 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 ' + # 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 } + /^\[/ { close_table() } + /^checksum = "sha256:[0-9a-f]+"/ { c = 1 } + END { close_table() }' mise.lock) + [ -z "$miss" ] || echo "mise.lock has no sha256 for: ${miss% }" +} +# 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; } + 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; } +# 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 + 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 +# 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' + 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 +} + +# 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[*]}" + + 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: $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 + + 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" + 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 ] || [ -f .mise.toml ] || [ -f .tool-versions ]; then + local bad; bad=$(mise_banned) + [ -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="" + 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)" + 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 "$gs (non-stub)"; fi + else warn "PV-W25 guix.scm missing — no reproducible Guix path"; fi + [ -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 "$(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 + 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)" + # 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)" + 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 + # 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 +} + +# 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 + 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 ($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 + 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" + # 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; } + 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 +} + +# 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" + [ -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 +} + +# 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 + 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}" + else + fail "PV-E40 neither guix nor mise is installed — see docs/SETUP.adoc"; return 1 + 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. +# 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; } + mise up --bump || return 1 + $T mise lock 2>/dev/null || mise lock || return 1 + local ch; ch=$(gpath channels.scm) + if [ -f "$ch" ]; then + hdr "guix: channel pin" + if have guix; then + 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="" + 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. 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; } + 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. +just_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" +} +# 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. + 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 +} +# Print AI-assisted setup instructions and attempt to copy the setup sentence. +# Clipboard failure is ignored. +cmd_ai_setup() { + cat <" >&2; return 2 ;; esac + 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 + fi + done + 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" + 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 +} + +# Print resolved repository metadata and provisioning configuration paths. +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=$(gpath "$f"); [ -f "$f" ] && printf "%s " "$f"; done)" + 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 + 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 ;; + crates-scm) cmd_crates_scm ;; + 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" ;; + 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/build/just/provision-modes.sh b/3-practice/provisioning/templates/build/just/provision-modes.sh new file mode 100755 index 00000000..eb9efa2f --- /dev/null +++ b/3-practice/provisioning/templates/build/just/provision-modes.sh @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# shellcheck shell=bash +# +# provision-modes.sh — the launcher-standard 0.6.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_or_return "$@" +# +# 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 +# 3-practice/provisioning/PROVISIONING-STANDARD.adoc + +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. +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")}" +} + +# Print linux, macos, windows or unknown based on the host kernel name. +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. +# 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 + echo "just is not installed — installing it with mise (mise use -g just@latest)..." >&2 + 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): + + 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 + 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 +# canon (which then runs that repo's *-local recipes). Only needs bash. +hp_lib() { (cd "$REPO_DIR" && bash build/just/provision-lib.sh "$@"); } + +# 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" +} + +# 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 ;; + --doctor) hp_lib doctor ;; + --heal) hp_lib heal ;; + --ai-setup) hp_lib ai-setup ;; + *) return 99 ;; + 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) + 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)" +} + +# Print launcher usage and the modes applicable to the repository archetype. +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 00000000..9f1ac7c4 --- /dev/null +++ b/3-practice/provisioning/templates/build/just/provision.just @@ -0,0 +1,108 @@ +# 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 {{quote(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, 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 + +# 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 + +# Fetch every detected language's dependencies +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 00000000..9b58c2db --- /dev/null +++ b/3-practice/provisioning/templates/docs/AI_INSTALLATION_GUIDE.adoc.tmpl @@ -0,0 +1,139 @@ +// 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: 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`. +__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 00000000..71de1d1e --- /dev/null +++ b/3-practice/provisioning/templates/docs/SETUP.adoc.tmpl @@ -0,0 +1,201 @@ +// 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 +`__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]] +== 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 +|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 +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 __GUIX_PREFIX__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 __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 +---- + +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]] +== 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-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 +|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 |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 +|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 +|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 +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 00000000..54ac61a2 --- /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 00000000..29c99984 --- /dev/null +++ b/3-practice/provisioning/templates/guix/guix.scm.cargo.tmpl @@ -0,0 +1,40 @@ +;; 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:)) + +;; 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: +;; 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 00000000..4555fe51 --- /dev/null +++ b/3-practice/provisioning/templates/guix/guix.scm.source.tmpl @@ -0,0 +1,44 @@ +;; 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:)) + +;; 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. +;; 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))))) + +(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 00000000..f465e7e8 --- /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__. 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 + (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 00000000..520fcffd --- /dev/null +++ b/3-practice/provisioning/templates/launcher.sh.tmpl @@ -0,0 +1,43 @@ +#!/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.6.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.6.0, archetype profile). Every mode is implemented in +# 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 + +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 00000000..c85e1187 --- /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: `__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. + +== 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 00000000..4295ea3f --- /dev/null +++ b/3-practice/provisioning/templates/llm-warmup-maintainer.adoc.tmpl @@ -0,0 +1,45 @@ +// 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, `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 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. + +== 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 00000000..64f149cd --- /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 00000000..be4bc98a --- /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 f4f818c6..5e138405 100644 --- a/docs/UX-standards/launcher-standard.adoc +++ b/docs/UX-standards/launcher-standard.adoc @@ -379,6 +379,95 @@ case "$MODE" in esac ---- +[[archetypes-and-provisioning]] +== Archetypes and Provisioning Modes (standard 0.6.0) + +Since standard version 0.6.0 (2026-10-01) *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 8533f721..59267535 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 9820bdc5..9186fef9 100644 --- a/launcher/launcher-standard_praxis.deed +++ b/launcher/launcher-standard_praxis.deed @@ -23,8 +23,11 @@ ;; format). Bumped 0.3.0 -> 0.4.0 because the resolution ladders below now ;; name the .deed file, which is consumer-visible; 0.4.0 -> 0.5.0 because ;; the new (js-runtime) clause is a consumer-visible obligation (D224). - :standard-version "0.5.0" - :standard-date "2026-09-30" + ;; Bumped 0.5.0 -> 0.6.0 (2026-10-01): every repository now carries a + ;; launcher, profiled by archetype, with a mandatory provisioning mode + ;; family (see `archetypes` and `provisioning-modes` below). + :standard-version "0.6.0" + :standard-date "2026-10-01" :compliance ("launcher-standard.adoc" "LM-LA-LIFECYCLE-STANDARD.adoc" "cross-platform-system-integration-modes" @@ -301,6 +304,42 @@ (tool :name panic-attack :style command :trigger on-start-failed :command "panic-attack assail {repo-dir}")) + ;; ---------------------------------------------------------------- archetypes + ;; Since 0.6.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`. diff --git a/scripts/check-launcher-standard-currency.sh b/scripts/check-launcher-standard-currency.sh index 56da284d..945e8ced 100755 --- a/scripts/check-launcher-standard-currency.sh +++ b/scripts/check-launcher-standard-currency.sh @@ -23,9 +23,9 @@ # A reference fails on the FILENAME or on the VERSION, separately: # # launcher-standard.a2ml any version -> FAIL (retired filename) -# launcher-standard_praxis.deed v0.4.0 -> FAIL (stale version) -# launcher-standard.a2ml v0.5.0 -> FAIL (filename only) -# launcher-standard_praxis.deed v0.5.0 -> PASS +# launcher-standard_praxis.deed v0.5.0 -> FAIL (stale version) +# launcher-standard.a2ml v0.6.0 -> FAIL (filename only) +# launcher-standard_praxis.deed v0.6.0 -> PASS # # A reference carrying no version token is checked on the filename alone; that # is not a defect in itself, because plenty of prose names the standard without @@ -62,7 +62,7 @@ set -uo pipefail CANONICAL_FILE="launcher-standard_praxis.deed" RETIRED_FILE="launcher-standard.a2ml" -CURRENT_VERSION="0.5.0" +CURRENT_VERSION="0.6.0" SELF_TEST_TMP="" # Path globs exempt as historical records. Matched against the repo-relative @@ -147,7 +147,7 @@ scan() { found="${BASH_REMATCH[3]}" # G1 -- THE TWO VERSIONS ARE NOT INTERCHANGEABLE, AND THIS GATE TRACKS ONE. # The header above says :schema-version is the GRAMMAR (1.0.0) and - # :standard-version is the DOCUMENT (0.5.0). This test used to accept any + # :standard-version is the DOCUMENT (0.6.0). This test used to accept any # number within 24 non-digit characters of the filename, so a line reading # `launcher-standard_praxis.deed` (DEED v1.0.0). Per-app config: # captured the GRAMMAR version and reported it as document drift -- the gate