From 3a8d8d3158584e8ea4208cdbf77abaf15c9d15e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 11:47:35 +0000 Subject: [PATCH 1/2] feat: follow the 2026-10-14 JS runtime ruling; re-sync the provisioning engine 0.6.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit standards LANGUAGE-POLICY §1.3–§1.4 (owner ruling 2026-10-14): Bun > Deno > pnpm > npm, from one package.json whose bun.lock, deno.lock and pnpm-lock.yaml are all written by `js-runtimes regen`. The template still taught and enforced the 2026-08-26 "Deno is retired" rule. runtime-policy.yml failed any repository with more than one lock, so it failed every repository that follows the ruling. Fixture runs, old gate against new: bun.lock + deno.lock + pnpm-lock.yaml old rc=1 "Mixed toolchains: 3" new rc=0 bun.lock + pnpm-lock.yaml old rc=1 new rc=0 package-lock.json + bun.lock old rc=1 new rc=1 deno.json + deno.lock old rc=0 warning new rc=0 notice empty / npm alone / bun.lockb alone unchanged The three js-runtimes locks now count as one dependency graph; an npm package-lock.json next to them is still mixed. Detection is unchanged. The provisioning engine (provision-lib.sh, provision-modes.sh, provision-check.sh) is copied from standards ed04132e (#1255); provision.just was already identical. Every line that only the template's old copy had was an older revision of canon code. provision-check.sh: 0 FAIL, 0 WARN. Doctor: the same results under both engines, apart from the PASS wording. The same policy appears in arrival-pack.ncl (CLAUDE.md regenerated by `just claude-md`; verify.sh in sync), mise.toml, docs/SETUP.adoc PV-W23/W30/W34 (the canon rows), the dev warm-up (the canon line), the three agent rule files, copilot-instructions.md, the PR template and two comments. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- .clinerules | 16 +- .cursorrules | 16 +- .github/copilot-instructions.md | 19 +- .github/pull_request_template.md | 2 +- .github/workflows/runtime-policy.yml | 46 ++-- .github/workflows/workflow-linter.yml | 2 +- .../arrival-pack/arrival-pack.ncl | 4 +- .windsurfrules | 16 +- CHANGELOG.adoc | 18 ++ CLAUDE.md | 4 +- build/just/provision-check.sh | 11 +- build/just/provision-lib.sh | 207 +++++++++++++++--- build/just/provision-modes.sh | 15 +- docs/SETUP.adoc | 6 +- docs/onboarding/llm-warmup-dev.adoc | 3 +- mise.toml | 3 +- scripts/check-action-pinning.js | 2 +- 17 files changed, 292 insertions(+), 98 deletions(-) diff --git a/.clinerules b/.clinerules index 18fd60b5..89d315b5 100644 --- a/.clinerules +++ b/.clinerules @@ -24,13 +24,15 @@ # Lean: sorry # Rust: transmute (unless FFI with // SAFETY: comment) -# JS RUNTIME — Bun (standards/3-practice/LANGUAGE-POLICY.adoc section 1) -# 1. Bun the JavaScript runtime: package.json + bun.lock, run with bun / bunx -# 2. pnpm only where an upstream toolchain needs a node_modules layout -# 3. npm last resort; permitted, never preferred — a noted decision -# Deno is RETIRED, not grandfathered (owner ruling 2026-08-26, standards#655): -# existing Deno projects migrate to Bun. TypeScript and ReScript are banned -# too; they migrate to AffineScript. Use plain JavaScript where JS is needed. +# JS RUNTIME — Bun first (standards/3-practice/LANGUAGE-POLICY.adoc sections 1.3-1.4) +# 1. Bun tier 1: package.json + bun.lock, run with bun / bunx +# 2. Deno tier 2: permitted; runs the same package.json scripts (deno task) +# 3. pnpm tier 3 +# 4. npm last resort; permitted, never preferred; keeps no lock +# One package.json carries every runtime's lock (bun.lock, deno.lock, +# pnpm-lock.yaml); `js-runtimes regen` writes all three (owner ruling +# 2026-10-14). TypeScript and ReScript are banned; they migrate to +# AffineScript. Use plain JavaScript where JS is needed. # BANNED LANGUAGES # Go -> Rust diff --git a/.cursorrules b/.cursorrules index d9cd1d21..0c183a87 100644 --- a/.cursorrules +++ b/.cursorrules @@ -22,13 +22,15 @@ # Lean: sorry # Rust: transmute (unless FFI with // SAFETY: comment) -# JS RUNTIME — Bun (standards/3-practice/LANGUAGE-POLICY.adoc section 1) -# 1. Bun the JavaScript runtime: package.json + bun.lock, run with bun / bunx -# 2. pnpm only where an upstream toolchain needs a node_modules layout -# 3. npm last resort; permitted, never preferred — a noted decision -# Deno is RETIRED, not grandfathered (owner ruling 2026-08-26, standards#655): -# existing Deno projects migrate to Bun. TypeScript and ReScript are banned -# too; they migrate to AffineScript. Use plain JavaScript where JS is needed. +# JS RUNTIME — Bun first (standards/3-practice/LANGUAGE-POLICY.adoc sections 1.3-1.4) +# 1. Bun tier 1: package.json + bun.lock, run with bun / bunx +# 2. Deno tier 2: permitted; runs the same package.json scripts (deno task) +# 3. pnpm tier 3 +# 4. npm last resort; permitted, never preferred; keeps no lock +# One package.json carries every runtime's lock (bun.lock, deno.lock, +# pnpm-lock.yaml); `js-runtimes regen` writes all three (owner ruling +# 2026-10-14). TypeScript and ReScript are banned; they migrate to +# AffineScript. Use plain JavaScript where JS is needed. # BANNED LANGUAGES # Go -> use Rust diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b1099794..8c3234ca 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -43,13 +43,17 @@ that can do the job: 1. **Bun** — default for all new work. Runs compiled ESM/JS directly, no bundler step. Uses an npm-compatible `package.json` plus `bun.lock`; both are expected, not anti-patterns. -2. **pnpm** — only where an upstream toolchain requires `node_modules`. -3. **npm** — last resort. Permitted, never preferred; a deliberate, noted choice. - -**Deno is being removed**, not grandfathered. Owner ruling 2026-08-26: *"deno is -to go and bun is the way we are going, put it first everywhere unless not -possible and explain why if not."* Existing Deno projects migrate to Bun; where -Bun genuinely cannot be used, document the reason in the repo. +2. **Deno** — permitted (tier 2). Deno 2 reads the same `package.json` and runs + its scripts with `deno task`. +3. **pnpm** — tier 3. +4. **npm** — last resort. Permitted, never preferred; it keeps no lock + (`package-lock.json` stays refused). + +**One manifest, every runtime** (owner ruling 2026-10-14, `LANGUAGE-POLICY.adoc` +§1.3–§1.4). `package.json` is the only manifest and carries every runtime's lock: +`bun.lock`, `deno.lock` and `pnpm-lock.yaml`. `js-runtimes regen` is the only +writer; run it after every `bun add`. Governance checks each runtime against its +lock. The ruling supersedes the 2026-08-26 one that removed Deno. **TypeScript is not the language for new application code — AffineScript is.** `LANGUAGE-POLICY.adoc` §1.2, ruled 2026-08-25, separates two questions the older @@ -64,7 +68,6 @@ migration destination is AffineScript. - No Go (use Rust) - No Python (use Julia or Rust) - No Nix (use Guix) -- No Deno for new work — being removed estate-wide; use Bun (owner ruling 2026-08-26) - No ReScript (`LANGUAGE-POLICY.adoc` §3) — migrate to AffineScript ## Containers diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 56136e1c..751230fa 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,7 +21,7 @@ Copyright (c) Jonathan D.A. Jewell - [ ] Tests pass (`just test` or equivalent) - [ ] Code is formatted (`just fmt` or equivalent) - [ ] Linter is clean (no new warnings or errors) -- [ ] No banned language patterns (no Python, Go, TypeScript, Deno or ReScript; JavaScript runs on Bun) +- [ ] No banned language patterns (no Python, Go, TypeScript or ReScript; JavaScript runs Bun > Deno > pnpm > npm) - [ ] No `unsafe` blocks without `// SAFETY:` comments - [ ] No banned functions (`believe_me`, `unsafeCoerce`, `Obj.magic`, `Admitted`, `sorry`) - [ ] SPDX license headers present on all new/modified source files diff --git a/.github/workflows/runtime-policy.yml b/.github/workflows/runtime-policy.yml index 7319b776..3036a808 100644 --- a/.github/workflows/runtime-policy.yml +++ b/.github/workflows/runtime-policy.yml @@ -2,8 +2,13 @@ # SPDX-License-Identifier: MPL-2.0 # Runtime and package-manager policy check. # -# Authority: hyperpolymath/standards LANGUAGE-POLICY.adoc §1. -# Ordering: Bun (1st) > pnpm (2nd) > npm (last resort). Deno is RETIRED, not a tier. +# Authority: hyperpolymath/standards LANGUAGE-POLICY.adoc §1.3–§1.4 (owner ruling +# 2026-10-14). Ordering: Bun (1st) > Deno (2nd) > pnpm (3rd) > npm (last resort). +# +# One manifest, every runtime: package.json is the only manifest, and +# `js-runtimes regen` writes bun.lock, deno.lock and pnpm-lock.yaml from it. Those +# three locks are ONE dependency graph locked three ways, not three toolchains; +# governance's "JS runtimes" jobs check each one against package.json. # # REPLACES npm-bun-blocker.yml, which failed any build carrying `bun.lockb` with # the message "npm/bun artifacts detected. Use Deno instead." That gate blocked @@ -11,11 +16,13 @@ # 55 repositories. # # What this fails on, deliberately: -# MIXED TOOLCHAINS -- two different package managers' lockfiles in one repo. +# MIXED TOOLCHAINS -- an npm package-lock.json next to the js-runtimes locks. # That is real, actionable drift: two dependency graphs that can disagree. # What it does NOT fail on: -# Using bun, pnpm or npm. Deno is RETIRED (2026-08-26). npm is LAST but PERMITTED; the check reports -# the tier in use so drift is visible without blocking legitimate work. +# The js-runtimes locks together, or any one runtime alone. npm is LAST but +# PERMITTED; a deno.json is a notice (Deno is tier 2, LANGUAGE-POLICY §1.3). +# The check reports the tiers in use so drift is visible without blocking +# legitimate work. name: Runtime Policy on: push: @@ -43,30 +50,33 @@ jobs: run: | set -euo pipefail - bun=0; deno=0; pnpm=0; npm=0 + bun=0; deno=0; pnpm=0; npm=0; deno_cfg=0 [ -f bun.lockb ] || [ -f bun.lock ] && bun=1 || true - [ -f deno.lock ] || [ -f deno.json ] || [ -f deno.jsonc ] && deno=1 || true + [ -f deno.lock ] && deno=1 || true + [ -f deno.json ] || [ -f deno.jsonc ] && deno_cfg=1 || true [ -f pnpm-lock.yaml ] && pnpm=1 || true [ -f package-lock.json ] && npm=1 || true - total=$((bun + deno + pnpm + npm)) + # bun, deno and pnpm locks are the one graph js-runtimes regen writes. + regen=$((bun + deno + pnpm)) - if [ "$total" -eq 0 ]; then - echo "::notice::No JS/TS package manager in use — nothing to check." + if [ $((regen + npm + deno_cfg)) -eq 0 ]; then + echo "::notice::No JS package manager in use — nothing to check." exit 0 fi - # Report the tier actually in use (LANGUAGE-POLICY.adoc §1). + # Report the tiers actually in use (LANGUAGE-POLICY.adoc §1). [ "$bun" -eq 1 ] && echo "Bun — tier 1 (preferred)" - [ "$deno" -eq 1 ] && echo "::warning::Deno lockfile/config present. Deno is BEING REMOVED, not grandfathered (owner ruling 2026-08-26, canon standards#655). Migrate to Bun — package.json + bun.lock. This is a warning rather than an error only while the estate migration is in flight; it escalates to an error when that completes." + [ "$deno" -eq 1 ] && echo "Deno — tier 2" + [ "$deno_cfg" -eq 1 ] && echo "::notice::A deno.json is authored for Deno (tier 2, permitted; notice only, LANGUAGE-POLICY.adoc §1.3). Keep scripts in package.json so Bun runs them too." [ "$pnpm" -eq 1 ] && echo "pnpm — tier 3" - [ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred. See LANGUAGE-POLICY.adoc §1." + [ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred, and it keeps no lock: package-lock.json is refused (standards#67). See LANGUAGE-POLICY.adoc §1.4." - if [ "$total" -gt 1 ]; then - echo "::error::Mixed toolchains: $total package managers have lockfiles in this repository." - echo "Two dependency graphs that can disagree is real drift. Pick one — preferring the" - echo "highest tier present — and delete the others' lockfiles." + if [ "$npm" -eq 1 ] && [ "$regen" -gt 0 ]; then + echo "::error::Mixed toolchains: package-lock.json sits next to the js-runtimes locks." + echo "Two dependency graphs that can disagree is real drift. Delete package-lock.json" + echo "and let 'js-runtimes regen' write bun.lock, deno.lock and pnpm-lock.yaml." exit 1 fi - echo "✅ Single package manager in use." + echo "✅ One dependency graph in use." diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index ef8483ad..5aefa7ba 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -67,7 +67,7 @@ jobs: echo "All workflows have permissions declared" # Bun is the estate's first-choice runtime (LANGUAGE-POLICY.adoc §1: - # Bun > pnpm > npm; Deno retired). Tag form + # Bun > Deno > pnpm > npm). Tag form # matches every other ref in this repo and is resolved by actions.lock. - name: Set up Bun uses: oven-sh/setup-bun@v2.2.0 diff --git a/.machine_readable/arrival-pack/arrival-pack.ncl b/.machine_readable/arrival-pack/arrival-pack.ncl index 8f8f378d..7c2be584 100644 --- a/.machine_readable/arrival-pack/arrival-pack.ncl +++ b/.machine_readable/arrival-pack/arrival-pack.ncl @@ -69,8 +69,8 @@ The descriptive family (working name *descriptiles*) describes what-is; the **co - **Before you invent, rename, or consolidate anything: STOP and check the map + IS-NOT.** ## Estate language policy (overridable per-repo via AGENTIC) -Deny: **Nix, Python, Go, TypeScript, Deno, ReScript, AGPL**. (Guix, not Nix.) -JavaScript runtime: **Bun** (package.json + bun.lock) > pnpm > npm (last resort, permitted). Deno is retired, not grandfathered (owner ruling 2026-08-26, standards#655): existing Deno projects migrate to Bun. +Deny: **Nix, Python, Go, TypeScript, ReScript, AGPL**. (Guix, not Nix.) +JavaScript runtime: **Bun** > Deno > pnpm > npm (last resort, permitted), from one `package.json` that carries every runtime's lock (`bun.lock`, `deno.lock`, `pnpm-lock.yaml`), all written by `js-runtimes regen` (owner ruling 2026-10-14, standards LANGUAGE-POLICY §1.3–§1.4). Deno is permitted again; `package-lock.json` stays refused. Use plain JavaScript when this tooling is needed. TypeScript and ReScript migrate to AffineScript. ## Asking the owner diff --git a/.windsurfrules b/.windsurfrules index b16b511b..10001182 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -24,13 +24,15 @@ # Lean: sorry # Rust: transmute (unless FFI with // SAFETY: comment) -# JS RUNTIME — Bun (standards/3-practice/LANGUAGE-POLICY.adoc section 1) -# 1. Bun the JavaScript runtime: package.json + bun.lock, run with bun / bunx -# 2. pnpm only where an upstream toolchain needs a node_modules layout -# 3. npm last resort; permitted, never preferred — a noted decision -# Deno is RETIRED, not grandfathered (owner ruling 2026-08-26, standards#655): -# existing Deno projects migrate to Bun. TypeScript and ReScript are banned -# too; they migrate to AffineScript. Use plain JavaScript where JS is needed. +# JS RUNTIME — Bun first (standards/3-practice/LANGUAGE-POLICY.adoc sections 1.3-1.4) +# 1. Bun tier 1: package.json + bun.lock, run with bun / bunx +# 2. Deno tier 2: permitted; runs the same package.json scripts (deno task) +# 3. pnpm tier 3 +# 4. npm last resort; permitted, never preferred; keeps no lock +# One package.json carries every runtime's lock (bun.lock, deno.lock, +# pnpm-lock.yaml); `js-runtimes regen` writes all three (owner ruling +# 2026-10-14). TypeScript and ReScript are banned; they migrate to +# AffineScript. Use plain JavaScript where JS is needed. # BANNED LANGUAGES # Go -> Rust diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index bd12b2ac..b2c2d6e4 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -43,6 +43,24 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning]. ==== Changed +* JavaScript runtimes follow the 2026-10-14 ruling (standards + LANGUAGE-POLICY §1.3–§1.4): Bun > Deno > pnpm > npm, from one + `package.json` whose `bun.lock`, `deno.lock` and `pnpm-lock.yaml` are all + written by `js-runtimes regen`. +** `.github/workflows/runtime-policy.yml` used to fail any repository + carrying more than one lock, so it failed every repository that follows the + ruling ("Mixed toolchains: 3 package managers"). Those three locks now count + as one dependency graph. `package-lock.json` next to them still fails. A + `deno.json` is a notice, not a warning. +** The provisioning engine (`build/just/provision-lib.sh`, + `provision-modes.sh`, `provision-check.sh`) is re-synced to standards + `ed04132e` (engine 0.6.0, standards#1255). A `package.json` provisions bun, + deno and pnpm, and deno and pnpm are no longer banned tools. +** The same policy appears in the arrival pack (`arrival-pack.ncl`, and + CLAUDE.md regenerated from it), `mise.toml`, `docs/SETUP.adoc` PV-W23, + PV-W30 and PV-W34, the dev warm-up, `.cursorrules`, `.clinerules`, + `.windsurfrules`, `.github/copilot-instructions.md` and the PR template. + * Canonical `.well-known/` location moved from the repository root to `www/.well-known/`. Well-known enforcement, Groove checks, the root-shape allowlist, the SSG bootstrap and the documentation now prefer the `www/` diff --git a/CLAUDE.md b/CLAUDE.md index 591d1753..c091568a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,8 +51,8 @@ The descriptive family (working name *descriptiles*) describes what-is; the **co - **Before you invent, rename, or consolidate anything: STOP and check the map + IS-NOT.** ## Estate language policy (overridable per-repo via AGENTIC) -Deny: **Nix, Python, Go, TypeScript, Deno, ReScript, AGPL**. (Guix, not Nix.) -JavaScript runtime: **Bun** (package.json + bun.lock) > pnpm > npm (last resort, permitted). Deno is retired, not grandfathered (owner ruling 2026-08-26, standards#655): existing Deno projects migrate to Bun. +Deny: **Nix, Python, Go, TypeScript, ReScript, AGPL**. (Guix, not Nix.) +JavaScript runtime: **Bun** > Deno > pnpm > npm (last resort, permitted), from one `package.json` that carries every runtime's lock (`bun.lock`, `deno.lock`, `pnpm-lock.yaml`), all written by `js-runtimes regen` (owner ruling 2026-10-14, standards LANGUAGE-POLICY §1.3–§1.4). Deno is permitted again; `package-lock.json` stays refused. Use plain JavaScript when this tooling is needed. TypeScript and ReScript migrate to AffineScript. ## Asking the owner diff --git a/build/just/provision-check.sh b/build/just/provision-check.sh index 26668422..36a855ac 100755 --- a/build/just/provision-check.sh +++ b/build/just/provision-check.sh @@ -20,7 +20,7 @@ REPO="${1:-.}" cd "$REPO" || { echo "provision-check: no such directory: $REPO" >&2; exit 1; } MIN_JUST="1.42.0" -VERBS="setup doctor heal dev-shell toolchain-refresh ai-setup ai-warmup eval config-show opsm build test bench lint fmt fmt-check run deps" +VERBS="setup doctor heal dev-shell toolchain-refresh ai-setup ai-warmup eval config-show opsm build test bench lint fmt fmt-check run deps" # hypatia:ignore eval_in_shell -- "eval" is a verb name, not the shell builtin # 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 @@ -28,16 +28,22 @@ VERBS="setup doctor heal dev-shell toolchain-refresh ai-setup ai-warmup eval con # Their locations come from provision-lib.sh (guix-dir, set-files): the engine and # this check must never disagree about where the Guix files or warm-ups live. [ -f "$LIB" ] || { echo "provision-check: engine missing: $LIB" >&2; exit 1; } +# Run the provisioning engine with the checked repository as its root. lib() { PROVISION_ROOT="$PWD" bash "$LIB" "$@"; } GDIR=$(lib guix-dir) SET_FILES=$(lib set-files) +# Print the path to Guix filename $1 using the resolved Guix directory. gp() { [ "$GDIR" = . ] && echo "$1" || echo "$GDIR/$1"; } fails=0 warns=0 +# Print a successful conformance check. ok() { printf ' ok %s\n' "$*"; } +# Print a failed conformance check and increment the failure count. bad() { printf ' FAIL %s\n' "$*"; fails=$((fails + 1)); } +# Print a conformance warning and increment the warning count. warn() { printf ' warn %s\n' "$*"; warns=$((warns + 1)); } +# Return success when version $1 is at least $2, using version sort order. version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } echo "[1] launcher" @@ -84,6 +90,9 @@ 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) diff --git a/build/just/provision-lib.sh b/build/just/provision-lib.sh index fb1d9426..1ceaaa8d 100755 --- a/build/just/provision-lib.sh +++ b/build/just/provision-lib.sh @@ -14,7 +14,7 @@ # # 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 +# ai-warmup eval config-show opsm lang-run (hypatia:ignore eval_in_shell -- verb list) # search version # facts: guix-specs mise-tools langs guix-dir set-files guix-gaps tool-table # system-deps @@ -24,7 +24,7 @@ # Exit: 0 ok | 1 a FAIL was found / a step failed | 2 usage error set -uo pipefail -PROVISION_LIB_VERSION="0.5.0" +PROVISION_LIB_VERSION="0.6.0" ROOT="${PROVISION_ROOT:-$(pwd)}" cd "$ROOT" || exit 2 DEED=".machine_readable/descriptiles/provisioning_praxis.deed" @@ -35,11 +35,17 @@ T="timeout 15" # some --version probes hang (observed 2026-09-30); never pr if [ -t 1 ]; then R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[34m'; Z=$'\033[0m'; else R=; G=; Y=; B=; Z=; fi PASS=0; WARN=0; FAIL=0 +# Increment the PASS tally and print the diagnostic message. pass() { PASS=$((PASS+1)); printf ' %sPASS%s %s\n' "$G" "$Z" "$*"; } +# Increment the WARN tally and print the diagnostic message. warn() { WARN=$((WARN+1)); printf ' %sWARN%s %s\n' "$Y" "$Z" "$*"; } +# Increment the FAIL tally and print the diagnostic message. fail() { FAIL=$((FAIL+1)); printf ' %sFAIL%s %s\n' "$R" "$Z" "$*"; } +# Print an informational message without changing diagnostic tallies. info() { printf ' %sinfo%s %s\n' "$B" "$Z" "$*"; } +# Print a section heading for provisioning output. hdr() { printf '\n%s== %s ==%s\n' "$B" "$*" "$Z"; } +# Return success when command $1 is available in the current shell. have() { command -v "$1" >/dev/null 2>&1; } # Where this repository keeps each part of the set. A file sits at the root only @@ -49,6 +55,7 @@ have() { command -v "$1" >/dev/null 2>&1; } # This is the ONE answer: provision-check.sh asks it through `guix-dir` and # `set-files` rather than keeping its own copy. guix_dir() { if [ -f build/guix.scm ]; then echo build; else echo .; fi; } +# Print the first directory containing warm-up guides, falling back to the root. warmup_dir() { local d for d in . docs/onboarding docs; do @@ -56,8 +63,11 @@ warmup_dir() { done echo . } +# Print the path to Guix filename $1 in the repository layout. gpath() { local d; d=$(guix_dir); [ "$d" = . ] && echo "$1" || echo "$d/$1"; } +# Print the warm-up guide path for audience $1 in the repository layout. wpath() { local d; d=$(warmup_dir); [ "$d" = . ] && echo "llm-warmup-$1.adoc" || echo "$d/llm-warmup-$1.adoc"; } +# List the provisioning-owned paths checked for unfilled template slots. set_files() { printf '%s\n' launcher.sh Justfile justfile mise.toml \ "$(gpath guix.scm)" "$(gpath manifest.scm)" "$(gpath channels.scm)" \ @@ -67,12 +77,14 @@ set_files() { # --------------------------------------------------------------------------- # Descriptor: flat `:key "value"` reads from provisioning_praxis.deed (s-expression). +# Use default $2 when the value is absent or empty; print without a trailing newline. deed() { # $1 key, $2 default local v="" [ -f "$DEED" ] && v=$(grep -oE "\(:?$1[[:space:]]+\"[^\"]*\"" "$DEED" 2>/dev/null | head -1 | sed -E 's/^[^"]*"//; s/"$//') [ -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[:/]} @@ -88,7 +100,10 @@ ARCHETYPE="$(deed archetype "")" # Zig to three (ffi/zig/build.zig, and rsr-template-repo's src/interface/ffi/build.zig: # 74 repos have their only build.zig at that depth, measured 2026-09-30). # Order matters only for display. `docs` is reported when nothing else is. +# Print the first path matching glob $1 within find depth $2 (default 2), +# excluding .git, node_modules and root target contents; no match prints nothing. first() { find . -maxdepth "${2:-2}" -not -path './.git/*' -not -path '*/node_modules/*' -not -path './target/*' -name "$1" -print -quit 2>/dev/null; } +# Print detected languages, one per line; use docs when no language marker exists. detect_langs() { local out=() [ -n "$(first Cargo.toml)" ] && out+=(rust) @@ -120,14 +135,18 @@ lang_tools() { gleam) echo "gleam erl" ;; ocaml) echo "opam dune ocaml" ;; haskell) echo "ghc cabal" ;; - bun) echo "bun" ;; + # One package.json, every runtime (LANGUAGE-POLICY §1.4): bun runs it, and + # `js-runtimes regen` needs deno and pnpm to write their locks too. + bun) echo "bun deno pnpm" ;; 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"). +# julia is 1.8.5: those come from mise, which Guix itself ships ("mise"). deno +# and pnpm come from mise as well (their Guix packages were not measured). 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" ;; @@ -144,9 +163,10 @@ lang_guix() { # 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 ;; + idris2) echo "idris2 (via pack)" ;; julia) echo julia ;; gleam) echo gleam ;; bun) echo "bun deno pnpm" ;; docs) echo lychee ;; esac } +# Print deduplicated base and detected-language Guix package specifications. guix_specs() { local l s=" $GUIX_BASE " for l in "${LANGS[@]}"; do for p in $(lang_guix "$l"); do case "$s" in *" $p "*) ;; *) s="$s$p ";; esac; done; done @@ -159,10 +179,12 @@ guix_specs() { MISE_BASE="just shellcheck" # Tools a repo's own recipes call (e.g. `deps-audit` runs trivy): pinned only where used. RECIPE_TOOLS="trivy" +# Print the mise tool names for language $1, if supported. lang_mise() { case "$1" in rust) echo "rust" ;; zig) echo "zig" ;; julia) echo "julia" ;; elixir) echo "erlang elixir" ;; gleam) echo "erlang gleam" ;; ocaml) echo "opam" ;; - bun) echo "bun" ;; docs) echo "lychee" ;; esac; } + bun) echo "bun deno pnpm" ;; docs) echo "lychee" ;; esac; } +# Print known recipe tools referenced outside comments in the repository Justfiles. recipe_tools() { local t f body="" for f in Justfile justfile build/just/*.just; do @@ -173,6 +195,7 @@ recipe_tools() { done } +# Print deduplicated mise tools needed by the base, detected languages and recipes. mise_tools() { local l t seen=" " out=() for t in $MISE_BASE $(for l in "${LANGS[@]}"; do lang_mise "$l"; done) $(recipe_tools); do @@ -181,6 +204,7 @@ mise_tools() { printf "%s\n" "${out[*]}" } +# Print installation guidance for the toolchain of language $1. lang_remedy() { case "$1" in rust) echo "mise use rust@latest (or rustup: https://rustup.rs)" ;; @@ -191,7 +215,7 @@ lang_remedy() { 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" ;; + bun) echo "mise use bun@latest deno@latest pnpm@latest" ;; esac } @@ -287,6 +311,8 @@ lang_cmd() { # $1 lang, $2 verb -> prints a shell command, or nothing (= N/A) # Run a contract verb across every detected language. N/A is reported, not # faked green: a verb with no command for any language exits 0 but SAYS so. +# A deed override runs alone and its status is returned. Otherwise all available +# language commands run; return the last failing status, or 0 if none failed. lang_run() { # $1 verb local verb=$1 ran=0 rc=0 l cmd override override=$(deed "$verb" "") @@ -312,33 +338,70 @@ lang_run() { # $1 verb # CI all see the same result. has_recipe() { have just && just --summary 2>/dev/null | tr " " "\n" | grep -qx "$1"; } +# Return success when version $1 is at least $2, using version sort order. version_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; } # Tools that must never appear in a repo's toolchain (estate language policy). -BANNED_TOOLS='python|deno|node|nodejs|npm|yarn|pnpm|typescript|rescript|make|black|ruff|pip|poetry|nix|go|golang|java|kotlin' +# deno and pnpm are permitted runtimes since the 2026-10-14 ruling (Bun > Deno > +# pnpm > npm); node stays banned, so npm, the last resort, is never provisioned. +BANNED_TOOLS='python|node|nodejs|npm|yarn|typescript|rescript|make|black|ruff|pip|poetry|nix|go|golang|java|kotlin' +BANNED_BACKENDS='npm|pipx|pip|go' # --------------------------------------------------------------------------- # Shared predicates. doctor and provision-check.sh both call THESE (the check via # `provision-lib.sh guix-stub|mise-lock-gaps|mise-banned`): a gate with its own # copy of a test passes what doctor warns about. -# The keys of mise.toml [tools], quotes stripped ("cargo:foo" stays cargo:foo). +# The tools every repository mise config declares, quotes stripped ("cargo:foo" +# stays cargo:foo): the [tools] keys of mise.toml and .mise.toml and the first +# word of each .tool-versions line. mise merges all three, so a predicate that +# read mise.toml alone would pass a banned tool pinned in the others. mise_toml_tools() { - [ -f mise.toml ] || return 0 - awk '/^\[tools\]/{t=1;next} /^\[/{t=0} t && /=/{sub(/[ \t]*=.*/,""); gsub(/["\x27 ]/,""); print}' mise.toml -} -# Banned tools named in mise.toml; a backend prefix does not hide one ("aqua:denoland/deno" is deno). + 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##*/} - [[ "$base" =~ ^($BANNED_TOOLS)$ ]] && hits="$hits$t " + 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% }" } -# Why mise.lock does not pin mise.toml, or nothing when it does. Presence is not -# enough: a zero-byte lock pins nothing. Every [tools] key needs its [[tools.]] -# entry, and the lock must carry checksums. +# Print the first category of lockfile gaps, or nothing if none is found. +# Skip checking when mise.toml is absent; otherwise check tools from all three +# config files for a non-empty version and platform tables for sha256 entries. +# Callers inspect stdout; version concreteness and checksum contents are not validated. mise_lock_gaps() { [ -f mise.toml ] || return 0 [ -f mise.lock ] || { echo "mise.lock missing"; return; } @@ -367,9 +430,10 @@ mise_lock_gaps() { END { close_table() }' mise.lock) [ -z "$miss" ] || echo "mise.lock has no sha256 for: ${miss% }" } -# Why a Guix file is a stub, or nothing when it is real. The test is positive: a -# guix.scm must define every field a package needs, not merely avoid known stub -# shapes; `(package (name "x") (source (local-file ".")))` is a stub. +# Print why Guix file $1 appears to be a stub, or nothing if the textual checks +# pass. Check required package fields as well as known stub shapes; this does +# not evaluate Scheme or verify that the package builds. Callers inspect stdout. +# For example, `(package (name "x") (source (local-file ".")))` is a stub. guix_stub_reason() { local f=$1 k miss="" [ -f "$f" ] || { echo "missing"; return; } @@ -402,11 +466,13 @@ lang_sysdeps() { case "$1" in ocaml) echo "gcc make patch unzip bubblewrap|build-essential patch unzip bubblewrap|xcode-select --install|opam compiles OCaml and sandboxes its builds with bubblewrap" ;; haskell) echo "gcc gcc-c++ gmp-devel make ncurses-devel xz perl|build-essential curl libffi-dev libgmp-dev libncurses-dev|xcode-select --install|ghcup installs GHC, which links through the C toolchain and GMP" ;; esac; } +# Print a comma-separated list of tools Guix cannot supply, or none. guix_gaps() { local l g gaps="" for l in "${LANGS[@]}"; do g=$(lang_guix_gap "$l"); [ -n "$g" ] && gaps="$gaps${gaps:+, }$g"; done printf '%s\n' "${gaps:-none}" } +# Print AsciiDoc table rows describing required tools and installation commands. tool_table() { local l t base for l in "${LANGS[@]}"; do @@ -417,6 +483,7 @@ tool_table() { echo "|(recipes) |\`${base// /\`, \`}\` |mise install, or your OS package manager (e.g. \`dnf install just ShellCheck\`)" } # shellcheck disable=SC2016 # the backticks are AsciiDoc literals, not command substitution +# Print OS dependency guidance for detected languages; $1 selects adoc or ai output. system_deps() { # $1 adoc|ai local l d seen="" fed deb mac why any=0 [ "$1" = adoc ] && printf '=== System packages\n' @@ -438,6 +505,11 @@ system_deps() { # $1 adoc|ai elif [ $any -eq 0 ]; then printf '* No other OS packages: every tool comes from mise.\n'; fi } +# Diagnose the provisioning set and toolchain, then the repository's own +# doctor-local checks. The PASS/WARN/FAIL tally is the last line of stdout; +# returns non-zero when anything FAILed. +# 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[*]}" @@ -477,9 +549,9 @@ cmd_doctor() { [ -z "$lg" ] && pass "mise.lock pins every mise.toml tool (latest → concrete, checksummed)" || warn "PV-W20 $lg — run: just toolchain-refresh" [ -f .mise.toml ] && [ -f mise.toml ] && warn "PV-W21 both mise.toml and .mise.toml — mise merges them; keep only mise.toml" [ -f .tool-versions ] && warn "PV-W22 .tool-versions present — a second toolchain source; fold it into mise.toml" - if [ -f mise.toml ]; then + if [ -f mise.toml ] || [ -f .mise.toml ] || [ -f .tool-versions ]; then local bad; bad=$(mise_banned) - [ -z "$bad" ] && pass "mise.toml pins no banned tool" || warn "PV-W23 mise.toml pins banned tool(s): $bad (language policy: bun, no python/deno/node/make)" + [ -z "$bad" ] && pass "the mise configs pin no banned tool" || warn "PV-W23 a mise config pins banned tool(s): $bad (language policy: Bun > Deno > pnpm; no python/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="" @@ -504,14 +576,16 @@ cmd_doctor() { 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; } + # deno.lock is a generated lock every package carries (`js-runtimes regen`); + # authoring in Deno is permitted (tier 2), so its manifests are a notice only. + for f in deno.json deno.jsonc import_map.json; do + [ -f "$f" ] && info "PV-W30 $f — authored for Deno (permitted, tier 2); package.json stays the one manifest every runtime reads" 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" + for f in package-lock.json yarn.lock tsconfig.json; do [ -f "$f" ] && { warn "PV-W34 $f — npm/yarn/TypeScript leftover; package.json + bun.lock, deno.lock and pnpm-lock.yaml (js-runtimes regen)"; any=1; }; done + [ $any -eq 0 ] && pass "no nix / make / python / npm leftovers" local hook=build/just/doctor-local.sh if [ -f "$hook" ]; then @@ -545,12 +619,17 @@ cmd_doctor() { printf '\n%s: %s%d PASS%s, %s%d WARN%s, %s%d FAIL%s\n' "$REPO_NAME" "$G" "$PASS" "$Z" "$Y" "$WARN" "$Z" "$R" "$FAIL" "$Z" if [ "$FAIL" -gt 0 ]; then - echo "Next: 'just heal' fixes what is safe to fix automatically; each FAIL code is explained in docs/SETUP.adoc §Troubleshooting." + # stderr, so the tally above stays the last line of stdout on every outcome. + echo "Next: 'just heal' fixes what is safe to fix automatically; each FAIL code is explained in docs/SETUP.adoc §Troubleshooting." >&2 return 1 fi return 0 } +# 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 @@ -583,6 +662,8 @@ cmd_setup() { 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" @@ -604,6 +685,9 @@ cmd_heal() { 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 @@ -617,9 +701,36 @@ cmd_dev_shell() { fi } +# Print the 40-hex commit of the `guix` channel in a channels list read from +# stdin (`guix describe --format=channels`), or nothing when there is none. +guix_channel_commit() { + awk '/\(name .guix\)/ { g = 1 } + g && match($0, /\(commit "[0-9a-f]{40}"\)/) { print substr($0, RSTART + 9, 40); exit }' +} + +# Re-pin only the `guix` channel's commit in channels file $1 to $2, keeping +# every other line (comments, other channels, introduction) as it is. Returns +# non-zero, leaving the file untouched, when it has no guix channel commit. +repin_guix_channel() { + local ch=$1 pin=$2 tmp + tmp=$(mktemp) || return 1 + if awk -v pin="$pin" ' + /\(name .guix\)/ { g = 1 } + g && !done && sub(/\(commit "[0-9a-f]{40}"\)/, "(commit \"" pin "\")") { done = 1 } + { print } + END { exit !done }' "$ch" > "$tmp"; then + cat "$tmp" > "$ch"; rm -f "$tmp" + else + rm -f "$tmp"; return 1 + fi +} + # Weekly toolchain refresh: bump mise pins and the lock, re-pin the Guix channel, # regenerate build/guix/crates.scm when guix.scm loads it, then show the diff # for a signed commit. +# 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; } @@ -629,8 +740,15 @@ cmd_toolchain_refresh() { if [ -f "$ch" ]; then hdr "guix: channel pin" if have guix; then - guix pull --channels="$ch" --dry-run >/dev/null 2>&1 || true - guix describe --format=channels > "$ch.new" 2>/dev/null && mv "$ch.new" "$ch" && info "$ch re-pinned to the current guix commit" + local pin + pin=$(guix describe --format=channels 2>/dev/null | guix_channel_commit) + if [ -z "$pin" ]; then + warn "could not read the current guix channel commit — $ch left unchanged" + elif repin_guix_channel "$ch" "$pin"; then + info "$ch: guix channel re-pinned to $pin" + else + warn "$ch has no (name 'guix) channel with a commit — left unchanged" + fi else info "guix not installed — $ch left as-is (CI re-pins it)"; fi fi local cr="" @@ -650,9 +768,11 @@ GUIX="${GUIX:-guix}" # Write build/guix/crates.scm: every registry crate in Cargo.lock as a Guix # origin, and %crate-inputs listing them for guix.scm. Written whole or not at -# all. The importer's output is accepted only when it defines exactly one crate -# source per registry package in Cargo.lock: run through a container, guix's exit -# status is lost, so the count is the check. +# all. Accept importer output when its rust- definition count matches the number +# of registry packages in Cargo.lock; the importer's exit status is not checked. +# GUIX may contain a command and arguments; import is limited to 1,800 seconds. +# Missing Cargo.lock is a successful no-op. Return 1 for temporary-file creation +# failure or a count mismatch; later write errors are not reliably propagated. cmd_crates_scm() { local dst=build/guix/crates.scm want got tmp spdx [ -f Cargo.lock ] || { info "no Cargo.lock — no crate inputs"; return 0; } @@ -699,6 +819,9 @@ just_say_it() { [ -z "$line" ] && line="Set up $REPO_NAME from https://github.com/$REPO_SLUG — follow docs/AI_INSTALLATION_GUIDE.adoc in that repo." printf '%s' "$line" } +# Copy stdin to an available clipboard with a three-second timeout; +# silently consume input and return 1 when no clipboard command is available. +# 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. @@ -710,6 +833,8 @@ clip() { # best-effort; silent when no clipboard exists (CI, SSH) else cat >/dev/null; return 1; fi timeout 3 "${c[@]}" >/dev/null 2>&1 } +# Print AI-assisted setup instructions and attempt to copy the setup sentence. +# Clipboard failure is ignored. cmd_ai_setup() { cat <" >&2; return 2 ;; esac @@ -739,10 +867,12 @@ cmd_ai_warmup() { fail "llm-warmup-$who.adoc not found"; return 1 } +# Run test and bench recipes, saving output and timings under .eval/; (hypatia:ignore eval_in_shell -- directory name) +# 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 + logf=".eval/$(date -u +%Y%m%dT%H%M%SZ).txt" # hypatia:ignore eval_in_shell -- .eval/ is a directory name + mkdir -p .eval # hypatia:ignore eval_in_shell -- .eval/ is a directory name { 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 @@ -751,7 +881,7 @@ cmd_eval() { 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. + # A skip is not a pass: lang_run exits 0 on N/A (users see success), eval says N/A. (hypatia:ignore eval_in_shell -- verb name) [ "$st" = PASS ] && grep -q "nothing was faked" "$o" && st=N/A rm -f "$o"; echo "$v: $st (${e}s)" | tee -a "$logf" done @@ -759,6 +889,7 @@ cmd_eval() { return $rc } +# Print resolved repository metadata and provisioning configuration paths. cmd_config_show() { echo "repo: $REPO_SLUG" echo "archetype: $ARCHETYPE" @@ -772,6 +903,7 @@ cmd_config_show() { echo "lib: provision-lib $PROVISION_LIB_VERSION" } +# Print repository-specific OPSM installation instructions. cmd_opsm() { cat <} if have agrep; then git ls-files -z | xargs -0 agrep -n -1 -- "$p" 2>/dev/null @@ -813,7 +948,7 @@ case "${1:-}" in crates-scm) cmd_crates_scm ;; ai-setup) cmd_ai_setup ;; ai-warmup) shift; cmd_ai_warmup "${1:-user}" ;; - eval) cmd_eval ;; + eval) cmd_eval ;; # hypatia:ignore eval_in_shell -- the "eval" verb, not the builtin config-show) cmd_config_show ;; opsm) cmd_opsm ;; search) shift; cmd_search "${1:-}" ;; diff --git a/build/just/provision-modes.sh b/build/just/provision-modes.sh index 35e60336..eb9efa2f 100755 --- a/build/just/provision-modes.sh +++ b/build/just/provision-modes.sh @@ -21,12 +21,14 @@ HP_PROVISION_MODES_VERSION="0.6.0" +# Print the flat deed value for key $1, falling back to default $2. hp__deed() { # $1 key, $2 default — flat (key "value") read from provisioning_praxis.deed local f="$REPO_DIR/.machine_readable/descriptiles/provisioning_praxis.deed" v="" [ -f "$f" ] && v=$(grep -oE "[(:]$1[[:space:]]+\"[^\"]*\"" "$f" | head -1 | sed -E 's/^[^"]*"//; s/"$//') printf '%s' "${v:-$2}" } +# Print the declared repository archetype, defaulting to library. hp_archetype() { hp__deed archetype "library"; } # Without a deed, the origin remote names the repo (a worktree or renamed clone # has another directory name); the directory is the last resort. @@ -36,6 +38,7 @@ hp_app_name() { hp__deed name "${u:-$(basename "$REPO_DIR")}" } +# Print linux, macos, windows or unknown based on the host kernel name. hp_platform() { case "$(uname -s)" in Linux*) echo linux ;; @@ -46,6 +49,8 @@ hp_platform() { } # Make sure `just` is runnable: PATH, then mise, then say exactly what to do. +# May install just globally via mise, extend PATH or define a just wrapper. +# Return 0 when available, otherwise 1 after printing installation guidance. hp_ensure_just() { command -v just >/dev/null 2>&1 && return 0 if command -v mise >/dev/null 2>&1; then @@ -68,13 +73,13 @@ 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 "$@"); } -# Returns the mode's exit code, or 99 when "$1" is not a provisioning mode. # The one-line hook for existing launchers: exit with a provisioning mode's # status, or return (0) so the caller's own mode switch runs. hp_provision_or_return() { @@ -84,6 +89,8 @@ hp_provision_or_return() { exit "$rc" } +# Run the provisioning mode named by $1 and return its status; +# return 99 when the mode is not handled here. hp_provision_dispatch() { case "${1:-}" in --setup) hp_lib setup ;; @@ -94,6 +101,7 @@ hp_provision_dispatch() { esac } +# Print the launcher name, version, commit, platform and architecture. hp_version_line() { local sha ver sha=$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown) @@ -102,6 +110,7 @@ hp_version_line() { printf '%s-launcher %s (%s) [%s-%s]\n' "$(hp_app_name)" "${ver#v}" "$sha" "$(hp_platform)" "$(uname -m)" } +# Print launcher usage and the modes applicable to the repository archetype. hp_help() { local name arch; name=$(hp_app_name); arch=$(hp_archetype) cat < Deno > pnpm > npm from one `package.json`; + after `bun add`, run `js-runtimes regen`. Python, TypeScript, ReScript, Nix, Go and Makefiles are banned. New code follows the repository's existing languages. * Every new file carries an SPDX header: `MPL-2.0` for code and config, `CC-BY-SA-4.0` for prose docs. Never change an existing file's licence. diff --git a/mise.toml b/mise.toml index 77030588..55154df7 100644 --- a/mise.toml +++ b/mise.toml @@ -5,7 +5,8 @@ # 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. +# Estate policy: no python, node/npm/yarn, make or nix here. JavaScript runs Bun > Deno > +# pnpm from one package.json, so a repository with one pins all three (LANGUAGE-POLICY §1.4). # # Idris2 is not a mise tool: it comes from pack (`just setup` says how). # A repository created from this template gets this file re-minted for its own diff --git a/scripts/check-action-pinning.js b/scripts/check-action-pinning.js index 94616131..80533f64 100644 --- a/scripts/check-action-pinning.js +++ b/scripts/check-action-pinning.js @@ -11,7 +11,7 @@ // inherited both the tag-form workflows and the gate that rejects them. // // Runtime: Bun — the estate's first-choice runtime per LANGUAGE-POLICY.adoc §1 -// (Bun > pnpm > npm; Deno retired). Plain JavaScript, not TypeScript: TypeScript is +// (Bun > Deno > pnpm > npm). Plain JavaScript, not TypeScript: TypeScript is // banned estate-wide (owner ruling; CLAUDE.md banned-languages table). Note // LANGUAGE-POLICY.adoc §1.2 currently claims TS is "permitted under Bun" — that // line is wrong and should be corrected; the ban stands. From 66453bf945de6e0ed6584c13bb2b93b28ef88275 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 11:51:39 +0000 Subject: [PATCH 2/2] ci: re-pin the standards reusables ae36881 -> 934b879; close actions.lock governance / Actions lockfile verify is red on main and on this PR: opening lockfile: ... lockfile version v0.0.3 is newer than this binary supports (v0.0.2) At ae36881 the gate installs gh-actions-lock v0.1.6. Standards 934b879 pins v0.2.0, which reads v0.0.3. This is the fix resource-algebra#8 (9240142) made: the six reusables (governance, hypatia-scan, mirror, rust-ci, scorecard, secret-scanner) move to 934b879. actions.lock was re-keyed with standards' own chain, complete-job-refs -> close-lock (offline gh stub) -> prune-stale. The standards record's nested uses: are completed at the new pin, and setup-deno@22d081f and setup-bun@0c5077e get records identical to resource-algebra main's. No orphan records. Locally: check-lock-sync.sh OK; the 934b879 lock gate with gh-actions-lock v0.2.0 reports "Immutable direct and transitive lockfile coverage verified." (exit 0); archetype_lock_merge_test.sh PASS; validate-template and workflow validation unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- .github/workflows/actions.lock | 30 +++++++++++++++++++--------- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/mirror.yml | 2 +- .github/workflows/rust-ci.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/secret-scanner.yml | 2 +- CHANGELOG.adoc | 5 +++++ 8 files changed, 32 insertions(+), 15 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index aa439690..ce241950 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -31,13 +31,13 @@ workflows: '.github/workflows/estate-rules.yml': - 'actions/checkout@v7.0.1' '.github/workflows/governance.yml': - - 'hyperpolymath/standards@ae36881b5ff4b469fb904b66542ac226a15107f3' + - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' '.github/workflows/groove-check.yml': - 'actions/checkout@v7.0.1' '.github/workflows/guix-policy.yml': - 'actions/checkout@v7.0.1' '.github/workflows/hypatia-scan.yml': - - 'hyperpolymath/standards@ae36881b5ff4b469fb904b66542ac226a15107f3' + - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' '.github/workflows/k9-validate.yml': - 'actions/checkout@v7.0.1' - 'hyperpolymath/k9-ecosystem@main' @@ -47,7 +47,7 @@ workflows: '.github/workflows/main-estate-audit.yml': - 'hyperpolymath/cicd-suite@55556cf5ba71ba744695861503c13148d3915a5d' '.github/workflows/mirror.yml': - - 'hyperpolymath/standards@ae36881b5ff4b469fb904b66542ac226a15107f3' + - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' '.github/workflows/ossf-best-practices.yml': - 'actions/checkout@v7.0.1' '.github/workflows/pages.yml': @@ -72,11 +72,11 @@ workflows: '.github/workflows/runtime-policy.yml': - 'actions/checkout@v7.0.1' '.github/workflows/rust-ci.yml': - - 'hyperpolymath/standards@ae36881b5ff4b469fb904b66542ac226a15107f3' + - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' '.github/workflows/scorecard.yml': - - 'hyperpolymath/standards@ae36881b5ff4b469fb904b66542ac226a15107f3' + - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' '.github/workflows/secret-scanner.yml': - - 'hyperpolymath/standards@ae36881b5ff4b469fb904b66542ac226a15107f3' + - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' '.github/workflows/security-policy.yml': - 'actions/checkout@v7.0.1' '.github/workflows/sonarqube.yml': @@ -160,6 +160,11 @@ dependencies: repo_id: 496012378 uses: - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' + 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': + ref: '22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + owner_id: 42048915 + repo_id: 356423100 'dependabot/fetch-metadata@v3.1.0': ref: 'v3.1.0' commit: 'sha1-25dd0e34f4fe68f24cc83900b1fe3fe149efef98' @@ -238,21 +243,23 @@ dependencies: commit: 'sha1-c1c9fa07992a02c1fd3d67a0dc1b08cccb852aef' owner_id: 6759885 repo_id: 1352485172 - 'hyperpolymath/standards@ae36881b5ff4b469fb904b66542ac226a15107f3': - ref: 'ae36881b5ff4b469fb904b66542ac226a15107f3' - commit: 'sha1-ae36881b5ff4b469fb904b66542ac226a15107f3' + 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82': + ref: '934b879f6068295e42dac20d80e010d555ebec82' + commit: 'sha1-934b879f6068295e42dac20d80e010d555ebec82' owner_id: 6759885 repo_id: 1116521501 uses: - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' - 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f' - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393' - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' - 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406' - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' - 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': @@ -260,6 +267,11 @@ dependencies: commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' owner_id: 67707773 repo_id: 421101922 + 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6': + ref: '0c5077e51419868618aeaa5fe8019c62421857d6' + commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' + owner_id: 108928776 + repo_id: 512644635 'oven-sh/setup-bun@v2.2.0': ref: 'v2.2.0' commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 45ab7dd0..8ef98da7 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -18,4 +18,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@ae36881b5ff4b469fb904b66542ac226a15107f3 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 394053de..e95d4ed3 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -27,4 +27,4 @@ permissions: jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@ae36881b5ff4b469fb904b66542ac226a15107f3 + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 268185a0..c118db04 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -13,7 +13,7 @@ permissions: contents: read jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@ae36881b5ff4b469fb904b66542ac226a15107f3 + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 # Explicit secrets map — no `secrets: inherit` (Hypatia WH008, alert #131). # All seven are the callee's complete optional contract (standards # mirror-reusable.yml@da2c748); behaviour is unchanged, future secrets diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 45a2b088..4f271320 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -16,4 +16,4 @@ permissions: contents: read jobs: rust-ci: - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@ae36881b5ff4b469fb904b66542ac226a15107f3 + uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5ce1f8f3..ff00f434 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -16,7 +16,7 @@ permissions: jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@ae36881b5ff4b469fb904b66542ac226a15107f3 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 # Reusable called-workflow permissions are CAPPED by the caller's grants; # without security-events: write here the scorecard SARIF upload fails with # startup_failure (hypatia WF018). id-token: write enables OIDC publish. diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 606b5c18..403f0b3c 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -26,4 +26,4 @@ jobs: # is therefore the cap GitHub applies to the called workflow. permissions: contents: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@ae36881b5ff4b469fb904b66542ac226a15107f3 + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index b2c2d6e4..1e4529f8 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -60,6 +60,11 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning]. CLAUDE.md regenerated from it), `mise.toml`, `docs/SETUP.adoc` PV-W23, PV-W30 and PV-W34, the dev warm-up, `.cursorrules`, `.clinerules`, `.windsurfrules`, `.github/copilot-instructions.md` and the PR template. +** The six standards reusables move from `ae36881` to `934b879`. At + `ae36881` governance's Actions lockfile verify installs gh-actions-lock + v0.1.6, which cannot read this lock's format v0.0.3 (exit 2, red on main). + `actions.lock` is re-keyed and closed at the new pin (setup-bun and + setup-deno records added), as resource-algebra#8 did. * Canonical `.well-known/` location moved from the repository root to `www/.well-known/`. Well-known enforcement, Groove checks, the root-shape