diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 772a4901e..fae672da2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -106,6 +106,7 @@ for the canonical statement. |---------------|----------|-------| | **AffineScript** | Primary application code | Compiles to typed-wasm; affine/linear types. Replaces ReScript across the estate (RS/TS/JS → AffineScript → typed-wasm). | | **Bun** | JS runtime & package management (tier 1) | 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. | +| **Deno** | JS runtime (tier 2) | Permitted, not gated (owner ruling 2026-10-14; order Bun > Deno > pnpm > npm). Reads the same `package.json`; every package carries a generated `deno.lock` (`tools/js-runtimes regen`, `3-practice/LANGUAGE-POLICY.adoc` §1.4). | | **Rust/SPARK** | Performance-critical, systems, WASM, CLI tools, safety-critical | "Rust" always means "Rust/SPARK" per terminology note above. Preferred over Ada where reachable. | | **Zig** | **APIs, FFIs, gateways, client SDKs (estate default 2026-05-28)**, memory-safe systems where Rust/SPARK is overkill | Zig is the estate-wide default for all API/FFI/gateway/client-SDK work unless explicitly special-cased; Idris2 owns ABIs. Completed V-lang→Zig migration 2026-05-28. | | **Idris2** | Formal verification (primary, ABI-style proofs) | ATS2 rejected. Proven-library status in `proven` repo. | @@ -159,6 +160,8 @@ for the canonical statement. > > ReScript's ban is uncontested, and its migration destination is **AffineScript**. +> **RULED 2026-10-14 — Deno is permitted again (tier 2).** The owner: *"deno does not need to be absolutely banned"*; the order is **Bun > Deno > pnpm > npm**, npm only as the last resort. The 2026-08-26 and 2026-09-22 rulings quoted below are **superseded**: Deno is back in ALLOWED, its refusal gate and the `deno-allow.txt` ledger are retired, and every package carries all the runtimes' locks through `tools/js-runtimes` (`3-practice/LANGUAGE-POLICY.adoc` §1.3–§1.4). Bun stays first everywhere, and each repository's environment (mise, containers, Guix) provisions it (`js-runtimes provision`). +> > **SUPERSEDED 2026-08-26 — Deno is no longer tier 2.** The owner ruled: > *"deno is to go and bun is the way we are going, put it first everywhere unless not > possible and explain why if not."* The "Bun > Deno > pnpm > npm" ordering above described @@ -177,9 +180,8 @@ for the canonical statement. | TypeScript | AffineScript | RS/TS/JS → AffineScript → typed-wasm. | | ReScript | AffineScript | RS/TS/JS → AffineScript → typed-wasm. | | **ReScript** | AffineScript | Banned in new code as of 2026-04-30. Existing `.res` files migrate to `.affine` directly (do not pass through ReScript). | -| **Deno** | Bun | **Banned 2026-09-22.** Owner ruling: *"deno is over, we're prioritising bun, and using bunx."* `deno.json` task definitions must be ported to `package.json` scripts. Shrink-only ledger: `.machine_readable/deno-allow.txt`. | | Node.js | Bun | Bun is Node-compatible; run the code, drop the runtime. | -| npm | Bun | npm is tier 3 — *permitted, never preferred*, not banned. `package-lock.json` must still not be tracked (standards#67). | +| npm | Bun | npm is tier 4 — the last resort: *permitted, never preferred*, not banned. `package-lock.json` must still not be tracked (standards#67). | | yarn | Bun | yarn is not in the tier list at all. | | Go | Rust/SPARK | | | **Python** | AffineScript/Rust/SPARK/Julia | Fully banned, no exceptions (SaltStack exception removed 2026-01-03) | @@ -241,7 +243,7 @@ Both are FOSS with independent governance (no Big Tech). - **Primary**: Guix (guix.scm) - **Nix**: retired — `flake.nix` is not accepted by Governance / Guix packaging policy; a repo with no `guix.scm` packages through a working Containerfile instead -- **JS deps**: **Bun** (`package.json` + `bun.lock`); `bunx ` to run one-off tooling +- **JS deps**: **Bun** (`package.json` + `bun.lock`); `bunx ` to run one-off tooling. `tools/js-runtimes regen` keeps `package.json` JCS and regenerates `bun.lock`, `deno.lock` and `pnpm-lock.yaml` together. ### Documentation Format diff --git a/.githooks/validate-lint-format.sh b/.githooks/validate-lint-format.sh index 7ecc4e545..8b25d5e91 100755 --- a/.githooks/validate-lint-format.sh +++ b/.githooks/validate-lint-format.sh @@ -18,8 +18,8 @@ # found" means a broken environment, not an exempt one. Skipping there # would report success having examined nothing. # -# * DENO is neither: it is BANNED, so no Deno toolchain is ever summoned. -# Its section refuses rather than checks — see it below. +# * DENO is neither: no Deno toolchain is summoned. Deno is tier 2 +# (permitted, not gated); its section records what it saw — see below. # # * OPTIONAL LINTER MISSING (hlint, fourmolu) → reported as an explicit # SKIP line, not as a pass, and CI remains the authority. These are add-ons @@ -146,40 +146,18 @@ else LEDGER+=("nickel: not staged") fi -# ── Deno (BANNED — a REFUSAL, not a lint) ─────────────────────────────── +# ── Deno (tier 2 — permitted, not gated) ──────────────────────────────── # -# Owner ruling: "deno is over, we're prioritising bun, and using bunx." -# -# This section used to install nothing and run `deno fmt --check` / `deno lint` -# over staged JS in a tree that tracked a deno.json, behind a `warn`. That -# lint-checked — and thereby blessed — the very thing the warning called -# banned, and it summoned a banned toolchain to do it. -# -# It now refuses. The question it asks is deliberately NARROWER than CI's: -# -# * CI asks "does this repository still carry Deno debt?", and answers it -# against the central shrink-only ledger -# `.machine_readable/deno-allow.txt` in `standards`. -# * This hook CANNOT ask that — it runs inside a caller's checkout and has -# no access to that ledger — and must not fake an answer. It asks instead -# "is THIS COMMIT ADDING Deno debt?", which is answerable right here. -# -# STAGED is `--diff-filter=ACM`, so a DELETED deno.json never appears: paying -# the debt down is never blocked, only growing it. That is the same -# shrink-only direction the central ledger enforces, reached without one. +# Owner ruling 2026-10-14: Deno is not banned; the order is Bun, Deno, pnpm, +# npm. From 2026-09-22 to 2026-10-14 this hook refused any commit that added +# or modified a deno.json(c); the ruling retired that refusal along with the +# central ledger. The hook now records what it saw and passes. DENO_CFG="$(staged_matching '(^|/)deno\.jsonc?$')" DENO_TRACKED="$(git ls-files -- 'deno.json' '*/deno.json' 'deno.jsonc' '*/deno.jsonc' | grep -c . || true)" if [ -n "$DENO_CFG" ]; then - fail "Deno is BANNED (owner ruling 2026-08-26 — Bun is the estate runtime). This commit ADDS or MODIFIES $(echo "$DENO_CFG" | grep -c .) Deno config file(s):" - while IFS= read -r cfg; do [ -n "$cfg" ] && echo " - $cfg" >&2; done <<< "$DENO_CFG" - echo " Port the deno.json 'tasks' map to package.json scripts run by" >&2 - echo " 'bun run' — 'bun run' does NOT read a deno.json tasks map — and" >&2 - echo " delete the config. Deleting a deno.json is never blocked here." >&2 - echo " A genuinely grandfathered repository is exempted CENTRALLY, in" >&2 - echo " .machine_readable/deno-allow.txt in hyperpolymath/standards, not" >&2 - echo " by a local bypass." >&2 + LEDGER+=("deno: $(echo "$DENO_CFG" | grep -c .) Deno config file(s) staged — permitted (tier 2; Bun preferred)") else - LEDGER+=("deno: no deno.json(c) added or modified (repo tracks ${DENO_TRACKED}; the ledgered CI gate is the authority on existing debt)") + LEDGER+=("deno: no deno.json(c) staged (repo tracks ${DENO_TRACKED})") fi # ⚠ Dropping the Deno lint leaves staged `.js`/`.jsx`/`.mjs`/`.cjs` with NO diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index d41887b41..f352968b3 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -68,8 +68,10 @@ workflows: '.github/workflows/governance-reusable.yml': - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393' - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' '.github/workflows/governance.yml': [] '.github/workflows/hypatia-scan-reusable.yml': - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' @@ -82,6 +84,10 @@ workflows: - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/instant-sync.yml': - 'peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697' + '.github/workflows/js-runtimes-reusable.yml': + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' '.github/workflows/k9-contractile.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/label-triage.yml': [] @@ -142,6 +148,8 @@ workflows: - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/self-test.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' '.github/workflows/settings-drift-detect.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' @@ -214,6 +222,11 @@ dependencies: repo_id: 496012378 uses: - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' + 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': + ref: 'v2.0.5' + commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + owner_id: 42048915 + repo_id: 356423100 'dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067': ref: 'v1' commit: 'sha1-7e38f4b43b4db5c8dd498af069a4f6196df1d067' diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 32a0805ca..9d2ec1478 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -32,16 +32,14 @@ # (owner, 2026-10-08). The `enable_sast` and `sast_blocking` inputs went # with it; no caller passed either. # -# Language-policy note: ReScript, V-lang and Deno are BANNED for new code -# (.claude/CLAUDE.md, owner rulings 2026-04-30 / 2026-04-10 / 2026-08-26). -# The ReScript and V jobs serve grandfathered trees still in migration and emit -# a ::warning:: saying so — a migration courtesy, not a licence. +# Language-policy note: ReScript and V-lang are BANNED for new code +# (.claude/CLAUDE.md, owner rulings 2026-04-30 / 2026-04-10). The ReScript and +# V jobs serve grandfathered trees still in migration and emit a ::warning:: +# saying so — a migration courtesy, not a licence. # -# DENO IS DIFFERENT, as of 2026-09-22. Its job no longer lints; it REFUSES, -# and the only way past it is the central shrink-only ledger -# `.machine_readable/deno-allow.txt` in `standards`. A ::warning:: cannot fail -# a job, so the old shape lint-checked — and thereby blessed — the very thing -# it called banned. +# Deno is NOT banned (owner ruling 2026-10-14: Bun, then Deno, then pnpm, then +# npm). Its job reports what it finds and passes; the refusal gate and the +# shrink-only ledger that stood from 2026-09-22 are retired. name: CI Pipeline (reusable) @@ -145,16 +143,13 @@ jobs: # a guard counting a different set than its consumer checks # (standards#1161). Nested Haskell is reported below as unsupported. N_HASKELL=$(count 'cabal.project' ':(glob)*.cabal') - # ⚠ DO NOT REMOVE THE DENO PROBE. It does not exist to lint Deno — - # it is the trigger for the REFUSAL job `deno` below, i.e. the teeth - # of the estate's standing no-Deno ruling, with slugs still live in - # `.machine_readable/deno-allow.txt`. Deleting the probe would not - # "drop support"; it would silently un-enforce a ban, because the - # refusal simply stops firing and the repo goes green. + # The Deno probe triggers the `deno` job below, which reports the + # repository's Deno configs (tier 2: permitted, not gated, owner + # ruling 2026-10-14). It used to trigger a refusal; the ban is lifted. N_DENO=$(count 'deno.json' '*/deno.json' 'deno.jsonc' '*/deno.jsonc') # TOTAL counts ONLY ecosystems some job in this file actually - # examines. Deno counts: a refusal IS a check. + # examines. Deno counts: its job lists and reports the configs it found. TOTAL=$(( N_RUST + N_NICKEL + N_RESCRIPT + N_V + N_HASKELL + N_DENO )) # ── DETECTED BUT UNSUPPORTED ──────────────────────────────────── @@ -209,9 +204,9 @@ jobs: probe 'Isabelle' 'ROOT' '*/ROOT' # Bun. `bun.lock*` and `bunfig.toml` are positive identification. # ⚠ `package.json` ALONE is an ASSUMPTION, stated here rather than - # hidden: the estate's standing ruling is bun-only, with Node and - # Deno banned, so a bare package.json in this estate is presumed to - # be a Bun manifest. That assumption is safe while Bun sits in THIS + # hidden: Bun is tier 1 (one package.json, read by every runtime), so + # a bare package.json in this estate is presumed to be a Bun + # manifest. That assumption is safe while Bun sits in THIS # list — it yields a refusal, never a pass. Re-examine it on the day # a Bun lint job is added, because then it would start selecting # which repositories get linted. @@ -456,146 +451,48 @@ jobs: exit "$FAILED" # ─────────────────────────────────────────────────────────────────────── - # Job 2 — Deno REFUSAL gate (BANNED; shrink-only ledger exemption) + # Job 2 — Deno (permitted, tier 2: a notice, not a refusal) # ─────────────────────────────────────────────────────────────────────── # - # Owner ruling: "deno is over, we're prioritising bun, and using bunx." - # This job used to install a pinned Deno binary and run `deno lint` / - # `deno fmt --check`, announcing the ban with a `::warning::`. A - # `::warning::` CANNOT fail a job, so that job asserted the opposite of what - # it said: it lint-checked, and thereby blessed, the very thing it called - # banned. It now REFUSES, and the only way past it is the central - # shrink-only ledger `.machine_readable/deno-allow.txt` in `standards`. - # - # Both outcomes print their denominator. A gate that only ever says yes - # proves nothing, and an exemption that reports nothing is a silent pass. + # Owner ruling 2026-10-14: Deno is not banned; the order is Bun, Deno, pnpm, + # npm (3-practice/LANGUAGE-POLICY.adoc §1). From 2026-09-22 to 2026-10-14 + # this job REFUSED any deno.json(c) unless the repository was on a central + # shrink-only ledger; the ruling retired both the refusal and the ledger. + # Like npm, Deno is permitted and not gated. The job still runs, and prints + # what it found, so the count stays visible rather than inferred. deno: - name: Deno (banned) + name: Deno (tier 2) needs: detect if: ${{ needs.detect.outputs.has_deno == 'true' }} runs-on: ${{ inputs.runs-on }} - timeout-minutes: 10 + timeout-minutes: 5 permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - # The ledger is CENTRAL: one file in `standards`, not a per-repo opt-out, - # so the estate's Deno debt is countable from one place and the ratchet - # can forbid its growth. Pinned to an immutable commit for the same - # reason as the governance helpers — `github.workflow_sha` resolves to - # the CALLER's commit, and following `main` would let an edit in - # `standards` change the verdict of every already-pinned caller with no - # review in their repositories. - - name: Checkout the pinned Standards Deno ledger - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: hyperpolymath/standards - # 938264d8 is the commit that introduced the ledger. ⚠ BUMP THIS - # whenever .machine_readable/deno-allow.txt changes, or the gate goes - # on judging callers against a stale list. A stale list is SAFE in - # one direction only: a repository that migrated off Deno stops - # tracking deno.json, so the job does not run at all and its lingering - # exemption never fires. A repository that is ADDED to the ledger - # stays red here until this pin moves — which is the correct default, - # because the addition needs a Ratchet-exception trailer anyway. - ref: 938264d8b0bbc3ca079e3d02efff5c1d423d0329 - path: .standards-deno-ledger - sparse-checkout: | - .machine_readable/deno-allow.txt - sparse-checkout-cone-mode: false - # Not fatal here: the next step names precisely what was missing and - # then FAILS CLOSED. A fetch failure must never read as an exemption. - # hypatia: allow research_extensions/RE005 -- deliberate fail-closed: "Refuse Deno unless this repository is ledgered" below checks for the ledger file and exits 1 with an explicit ::error:: if missing, regardless of why. - continue-on-error: true - - - name: Refuse Deno unless this repository is ledgered + - name: Report Deno configuration shell: bash env: - LEDGER: .standards-deno-ledger/.machine_readable/deno-allow.txt - SLUG: ${{ github.repository }} N_DENO: ${{ needs.detect.outputs.n_deno }} run: | - # LEDGER, SLUG and N_DENO come from the step `env:` map above. set -euo pipefail - - # FAIL CLOSED. "Could not read the ledger" is not "is exempt". - if [ ! -f "$LEDGER" ]; then - echo "::error::Deno gate: the central ledger could not be read ($LEDGER). This is NOT an exemption — the gate refuses rather than guess. Check the pinned ref of the 'Checkout the pinned Standards Deno ledger' step." - { - echo "### Deno (banned)" - echo "" - echo "**REFUSED: the exemption ledger could not be read.** A gate that cannot" - echo "check its exemption list must not pass; this is not an exemption." - echo "" - } >> "$GITHUB_STEP_SUMMARY" - exit 1 - fi - - # Strip comments and blanks; this is also the ledger's own denominator. - ALLOWED="$(grep -vE '^[[:space:]]*(#|$)' "$LEDGER" || true)" - N_ALLOWED="$(printf '%s' "$ALLOWED" | grep -c . || true)" - - # What this repository actually tracks, named rather than counted, so - # the migration has a list to work from. CONFIGS="$(git ls-files -- 'deno.json' '*/deno.json' 'deno.jsonc' '*/deno.jsonc')" - - echo "Deno debt: ${N_ALLOWED} ledgered repositories estate-wide." - echo "This repository (${SLUG}) tracks ${N_DENO} Deno config file(s):" - printf '%s\n' "$CONFIGS" | sed 's/^/ - /' - - if printf '%s\n' "$ALLOWED" | grep -Fxq "$SLUG"; then - echo "::notice::${SLUG} is on the Deno exemption ledger (${N_ALLOWED} repositories). Deno remains BANNED: this is grandfathered debt, not a licence. Port the deno.json 'tasks' map to package.json scripts run by 'bun run', delete the config, then delete this repository's line from .machine_readable/deno-allow.txt." - { - echo "### Deno (banned) — ledgered debt" - echo "" - echo "| | |" - echo "|---|---|" - echo "| Repository | \`${SLUG}\` |" - echo "| Deno configs tracked here | **${N_DENO}** |" - echo "| Ledgered repositories estate-wide | **${N_ALLOWED}** |" - echo "| Verdict | exempt (shrink-only ledger) |" - echo "" - echo "Tracked configs:" - echo "" - # Backticks below are literal markdown, not command substitution. - # shellcheck disable=SC2016 - printf '%s\n' "$CONFIGS" | sed 's/^/- `/; s/$/`/' - echo "" - echo "_Exempt is not approved._ Deno is banned estate-wide. The ledger is" - echo "SHRINK-ONLY: \`scripts/check-exemption-ratchet.sh\` fails any commit that" - echo "adds a line to it without a \`Ratchet-exception\` trailer naming the file." - echo "" - } >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - - echo "::error::Deno is BANNED (owner ruling: 'deno is over, we're prioritising bun, and using bunx'). ${SLUG} tracks ${N_DENO} Deno config file(s) and is NOT on the exemption ledger (${N_ALLOWED} repositories listed). Migrate to Bun: port the deno.json 'tasks' map to package.json scripts run by 'bun run' — 'bun run' does not read a deno.json tasks map — and delete the config." + echo "::notice::This repository tracks ${N_DENO} Deno config file(s). Deno is tier 2 (Bun first; then Deno, pnpm, npm). The estate model is one package.json that every runtime reads; Deno 2 reads it natively, and 'bun run' does not read a deno.json tasks map." { - echo "### Deno (banned) — REFUSED" + echo "### Deno (tier 2)" echo "" echo "| | |" echo "|---|---|" - echo "| Repository | \`${SLUG}\` |" echo "| Deno configs tracked here | **${N_DENO}** |" - echo "| Ledgered repositories estate-wide | **${N_ALLOWED}** |" - echo "| Verdict | **REFUSED — not ledgered** |" - echo "" - echo "Tracked configs:" + echo "| Verdict | permitted (tier 2; Bun preferred) |" echo "" # Backticks below are literal markdown, not command substitution. # shellcheck disable=SC2016 printf '%s\n' "$CONFIGS" | sed 's/^/- `/; s/$/`/' echo "" - echo "Deno is banned estate-wide. Migrate to Bun, or — if this is genuinely" - echo "grandfathered debt — add \`${SLUG}\` to" - echo "\`.machine_readable/deno-allow.txt\` in \`hyperpolymath/standards\` with a" - echo "\`Ratchet-exception: .machine_readable/deno-allow.txt — \` commit" - echo "trailer, and bump this workflow's ledger pin." - echo "" } >> "$GITHUB_STEP_SUMMARY" - exit 1 # ─────────────────────────────────────────────────────────────────────── # Job 2 — ReScript (grandfathered; banned for new code) # ─────────────────────────────────────────────────────────────────────── @@ -870,7 +767,7 @@ jobs: row "Secret scanning" "$R_SECRET" "n/a" row "Rust" "$R_RUST" "no Cargo.toml tracked (has_rust=${H_RUST})" row "Nickel" "$R_NICKEL" "no Nickel tracked, *.k9.ncl excluded (has_nickel=${H_NICKEL})" - row "Deno" "$R_DENO" "BANNED — refusal gate; skipped means no deno.json(c) tracked (has_deno=${H_DENO})" + row "Deno" "$R_DENO" "tier 2, reported not gated; skipped means no deno.json(c) tracked (has_deno=${H_DENO})" row "ReScript" "$R_RESCRIPT" "no rescript.json/bsconfig.json/*.res tracked (has_rescript=${H_RESCRIPT})" row "V" "$R_V" "no v.mod tracked (has_v=${H_V})" row "Haskell" "$R_HASKELL" "no root cabal.project/*.cabal tracked (has_haskell=${H_HASKELL})" diff --git a/.github/workflows/governance-reusable.yml b/.github/workflows/governance-reusable.yml index 51a58ea3e..ab4d9a446 100644 --- a/.github/workflows/governance-reusable.yml +++ b/.github/workflows/governance-reusable.yml @@ -659,7 +659,7 @@ jobs: echo "✅ package.json runtime dependencies accompanied by a Bun lockfile — tier 1, fine." else echo "❌ package.json with runtime \"dependencies\" and no Bun lockfile." - echo " Adopt Bun (tier 1) or declare imports in deno.json (tier 2)." + echo " Bun is tier 1: add bun.lock (tools/js-runtimes regen writes it, with deno.lock and pnpm-lock.yaml)." FAILED=1 fi fi @@ -1716,3 +1716,112 @@ jobs: fi bash "$RUNNER_TEMP/check-debt-ratchet.sh" \ "${{ github.event.pull_request.base.sha }}" + + # ─────────────────────────────────────────────────────────────────────── + # One package.json, every JavaScript runtime (3-practice/LANGUAGE-POLICY.adoc + # §1.4; owner ruling 2026-10-14: Bun, then Deno, then pnpm, then npm, and + # "I want all to have it"). The checks live in tools/js-runtimes (Rust); the + # same jobs, with the same pins, are offered standalone as + # js-runtimes-reusable.yml. A repository with no package.json passes both + # with a notice naming the denominator (0). + # ─────────────────────────────────────────────────────────────────────── + js-runtimes-provision: + name: JS runtimes — environment provisions Bun first + runs-on: ${{ inputs.runs-on }} + timeout-minutes: 15 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ github.repository }} + ref: ${{ github.sha }} + persist-credentials: false + - name: Checkout standards for the checker + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/standards + ref: ${{ job.workflow_sha }} + path: .standards-js + persist-credentials: false + sparse-checkout: | + tools/js-runtimes + sparse-checkout-cone-mode: false + - name: Build js-runtimes + run: | + set -euo pipefail + # Bootstrap rule (as debt-ratchet): standards builds the tool from + # the PR's own tree; every other caller from the pinned checkout, + # never from a same-named directory of its own. + if [ "${{ github.repository }}" = "hyperpolymath/standards" ]; then SRC=tools/js-runtimes; else SRC=.standards-js/tools/js-runtimes; fi + cargo install --quiet --locked --path "$SRC" --root "$RUNNER_TEMP/jsr" + rm -rf .standards-js + - name: Environment definitions provision Bun first + run: '"$RUNNER_TEMP/jsr/bin/js-runtimes" provision .' + + js-runtimes: + name: JS runtimes — ${{ matrix.runtime }} + runs-on: ${{ inputs.runs-on }} + timeout-minutes: 20 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + runtime: [bun, deno, pnpm] + env: + # pnpm 10.28.0, pinned by its npm tarball's sha512 (hex of dist.integrity). + PNPM_PIN: pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ github.repository }} + ref: ${{ github.sha }} + persist-credentials: false + - name: Is there a package.json? + id: has + env: + RUNTIME: ${{ matrix.runtime }} + run: | + if [ -f package.json ]; then + echo "pkg=true" >> "$GITHUB_OUTPUT" + else + echo "pkg=false" >> "$GITHUB_OUTPUT" + echo "::notice::No package.json: 0 JavaScript manifests, nothing for the $RUNTIME leg to check." + fi + - name: Checkout standards for the checker + if: steps.has.outputs.pkg == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/standards + ref: ${{ job.workflow_sha }} + path: .standards-js + persist-credentials: false + sparse-checkout: | + tools/js-runtimes + sparse-checkout-cone-mode: false + - name: Set up Bun + if: steps.has.outputs.pkg == 'true' && matrix.runtime == 'bun' + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.4.2" + - name: Set up Deno + if: steps.has.outputs.pkg == 'true' && matrix.runtime == 'deno' + uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5 + with: + deno-version: "2.9.6" + - name: Set up pnpm (Corepack, hash-pinned) + if: steps.has.outputs.pkg == 'true' && matrix.runtime == 'pnpm' + run: corepack enable pnpm && corepack install -g "$PNPM_PIN" + - name: Build js-runtimes + if: steps.has.outputs.pkg == 'true' + run: | + set -euo pipefail + if [ "${{ github.repository }}" = "hyperpolymath/standards" ]; then SRC=tools/js-runtimes; else SRC=.standards-js/tools/js-runtimes; fi + cargo install --quiet --locked --path "$SRC" --root "$RUNNER_TEMP/jsr" + rm -rf .standards-js + - name: One manifest, ${{ matrix.runtime }} + if: steps.has.outputs.pkg == 'true' + env: + RUNTIME: ${{ matrix.runtime }} + run: '"$RUNNER_TEMP/jsr/bin/js-runtimes" check "$RUNTIME" .' diff --git a/.github/workflows/js-runtimes-reusable.yml b/.github/workflows/js-runtimes-reusable.yml new file mode 100644 index 000000000..8e2dc1e00 --- /dev/null +++ b/.github/workflows/js-runtimes-reusable.yml @@ -0,0 +1,138 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# One package.json, every JavaScript runtime (3-practice/LANGUAGE-POLICY.adoc +# §1.4). Preference order: Bun, Deno, pnpm, npm (owner ruling 2026-10-14). +# +# For a caller with a package.json: +# * provision — every environment definition the repository carries +# (mise.toml, Containerfiles, Guix manifests) provisions Bun, or, where +# Bun is not always available (Guix), the best runtime it can; +# * one job per locked runtime (bun, deno, pnpm) — package.json is I-JSON + +# JCS (D306), that runtime's lock exists and installs FROZEN, and the +# `test` script passes under that runtime's own runner. +# Every repository carries all three legs (owner: "I want all to have it"). +# A caller without a package.json passes with a notice that names the +# denominator (0), never silently. +# +# The checker is tools/js-runtimes (Rust) at THIS workflow's own commit, so it +# is exactly what the caller pinned. The fix for any lock or JCS finding is +# `js-runtimes regen`. +# +# Usage (caller): +# jobs: +# js-runtimes: +# uses: hyperpolymath/standards/.github/workflows/js-runtimes-reusable.yml@ +# # with: +# # working-directory: web # default "." +# +# Runtimes are pinned: Bun and Deno by SHA-pinned setup actions, pnpm by +# Corepack with the tarball's sha512 (a wrong hash is refused). +name: JS runtimes (reusable) + +on: + workflow_call: + inputs: + working-directory: + description: Directory holding package.json + type: string + default: "." + +permissions: + contents: read + +jobs: + provision: + name: provision + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout caller repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Checkout standards for the checker + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/standards + # This reusable's own commit (the pattern governance-reusable uses). + ref: ${{ job.workflow_sha }} + path: .standards-js + persist-credentials: false + sparse-checkout: | + tools/js-runtimes + sparse-checkout-cone-mode: false + - name: Build js-runtimes + run: | + set -euo pipefail + cargo install --quiet --locked --path .standards-js/tools/js-runtimes --root "$RUNNER_TEMP/jsr" + rm -rf .standards-js + - name: Environment definitions provision Bun first + env: + WORKDIR: ${{ inputs.working-directory }} + run: '"$RUNNER_TEMP/jsr/bin/js-runtimes" provision "$WORKDIR"' + + check: + name: ${{ matrix.runtime }} + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + runtime: [bun, deno, pnpm] + env: + # pnpm 10.28.0, pinned by its npm tarball's sha512 (hex of dist.integrity). + PNPM_PIN: pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48 + steps: + - name: Checkout caller repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Is there a package.json? + id: has + env: + WORKDIR: ${{ inputs.working-directory }} + RUNTIME: ${{ matrix.runtime }} + run: | + if [ -f "$WORKDIR/package.json" ]; then + echo "pkg=true" >> "$GITHUB_OUTPUT" + else + echo "pkg=false" >> "$GITHUB_OUTPUT" + echo "::notice::No package.json in '$WORKDIR': 0 JavaScript manifests, nothing for the $RUNTIME leg to check." + fi + - name: Checkout standards for the checker + if: steps.has.outputs.pkg == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/standards + ref: ${{ job.workflow_sha }} + path: .standards-js + persist-credentials: false + sparse-checkout: | + tools/js-runtimes + sparse-checkout-cone-mode: false + - name: Set up Bun + if: steps.has.outputs.pkg == 'true' && matrix.runtime == 'bun' + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.4.2" + - name: Set up Deno + if: steps.has.outputs.pkg == 'true' && matrix.runtime == 'deno' + uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5 + with: + deno-version: "2.9.6" + - name: Set up pnpm (Corepack, hash-pinned) + if: steps.has.outputs.pkg == 'true' && matrix.runtime == 'pnpm' + run: corepack enable pnpm && corepack install -g "$PNPM_PIN" + - name: Build js-runtimes + if: steps.has.outputs.pkg == 'true' + run: | + set -euo pipefail + cargo install --quiet --locked --path .standards-js/tools/js-runtimes --root "$RUNNER_TEMP/jsr" + rm -rf .standards-js + - name: One manifest, ${{ matrix.runtime }} + if: steps.has.outputs.pkg == 'true' + env: + RUNTIME: ${{ matrix.runtime }} + WORKDIR: ${{ inputs.working-directory }} + run: '"$RUNNER_TEMP/jsr/bin/js-runtimes" check "$RUNTIME" "$WORKDIR"' diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml index b986b2d2a..ce076e5e8 100644 --- a/.github/workflows/self-test.yml +++ b/.github/workflows/self-test.yml @@ -55,6 +55,20 @@ jobs: - name: Install ijson-jcs run: cargo install --git https://github.com/hyperpolymath/ijson-jcs --rev 0fdeea716fd558e9807362acf6f8202d56d8d88f --locked ijson-jcs + # scripts/tests/js-runtimes-test.sh needs all three runtimes; it exits 2 + # without them. Same pins as js-runtimes-reusable.yml (keep in step). + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.4.2" + - name: Set up Deno + uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5 + with: + deno-version: "2.9.6" + - name: Set up pnpm (Corepack, hash-pinned) + env: + PNPM_PIN: pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48 + run: corepack enable pnpm && corepack install -g "$PNPM_PIN" # scripts/tests/gh-actions-lock-contract-test.sh runs the real binary; it # exits 2 without it. The version must match every `--pin` in this tree # (the test fails when they disagree); the hash is the release asset's. diff --git a/.machine_readable/deno-allow.txt b/.machine_readable/deno-allow.txt deleted file mode 100644 index a48f4423b..000000000 --- a/.machine_readable/deno-allow.txt +++ /dev/null @@ -1,78 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# deno-allow.txt — SHRINK-ONLY exemption ledger for the Deno refusal gate -# (the `deno` job of .github/workflows/ci-pipeline.yml). -# -# Owner ruling: "deno is over, we're prioritising bun, and using bunx." -# Deno is BANNED. A repository that still tracks `deno.json`/`deno.jsonc` goes -# RED unless its `owner/name` slug is listed below; a listed repository gets a -# `::notice::` naming its debt and the job passes. Both paths print the -# denominator, so an exemption is never a silent pass. -# -# One `owner/name` slug per line, sorted. The gate compares -# `${{ github.repository }}` — a SLUG, not a path. Local checkout paths are not -# a valid key: the estate's ~240 linked worktrees check the same repository out -# many times over. -# -# ⚠ SHRINK-ONLY. Registered in LEDGERS=() in scripts/check-exemption-ratchet.sh, -# which line-counts this file against BASE_REF and FAILS on growth unless a -# commit in the range carries a trailer naming this exact path: -# -# Ratchet-exception: .machine_readable/deno-allow.txt — -# -# To leave the ledger: delete the repository's `deno.json`/`deno.jsonc`, port -# the `tasks` map to `package.json` scripts run by `bun run`, then delete its -# line here. ⚠ `bun run` does NOT read a `deno.json` `tasks` map; that port is -# the substantive part of the migration, not the config deletion. -# -# ⚠ A repository TRANSFER changes its slug (measured: `MaridIR.jl` moved -# hyperpolymath → metadatastician). Transferring a ledgered repository turns it -# red until the new slug is added — which is growth, and therefore needs the -# trailer above. -# -# ── Seed provenance ─────────────────────────────────────────────────────────── -# Measured 2026-09-22. COMPLETE CENSUS, not a sample: all 458 non-archived -# repositories owned by the two accounts (409 hyperpolymath, 49 metadatastician, -# enumerated with `gh repo list --limit 2000 --no-archived`), each -# probed on its DEFAULT BRANCH with -# gh api repos//git/trees/?recursive=1 -# and matched against the regex `(^|/)deno\.jsonc?$` at ANY depth. -# -# Result: 11 carry a Deno config, 446 do not, 1 has no commits -# (metadatastician/proglanging-languages), 0 probe errors. -# -# Two controls, because a census that cannot fail is not a measurement: -# * per-repo denominator — each probe recorded its tree entry count, so a -# failed probe reports ERR and can never be mistaken for "no Deno". This -# caught a total probe failure (458/458 ERR) that would otherwise have been -# published as "zero Deno estate-wide". -# * truncation — GitHub silently truncates a recursive tree at ~100k entries. -# `hyperpolymath/julia-ecosystem` came back `"truncated": true` (69117 -# entries), so its result was DISCARDED and re-measured with a blobless -# clone (`--filter=tree:0 --depth 1`): 57611 blobs, zero matches. Every -# other tree reported `"truncated": false`. -# -# ⚠ Do NOT regenerate this list from the local working estate. The plan this -# implements cited 69 repositories, derived from tracked `deno.json` files in -# local checkouts. That figure is WRONG and is withdrawn: the local worktrees -# are stale (last fetch 2026-09-15) while the default branches had already -# removed these configs — e.g. hyperpolymath/coq-jr carries `deno.json` on local -# `main` and not on `origin/main`, removed by `44f956e refactor: eradicate -# TypeScript and NPM/Deno configs (#80)`. Only default-branch content is a valid -# proxy for what CI executes. -# -# ⚠ `deno.jsonc` matched ZERO repositories. The detection glob still covers it -# anyway: this is a REFUSAL gate, and a gate that checks only one of the two -# spellings of the thing it bans is exactly the vacuous class it exists to stop. -hyperpolymath/ambientops -hyperpolymath/civic-connect -hyperpolymath/excel-economic-numbers-tool -hyperpolymath/proven -hyperpolymath/social-media-tools -metadatastician/burble -metadatastician/gossamer -metadatastician/selur -metadatastician/stapeln -metadatastician/svalinn -metadatastician/vordr diff --git a/0-canon/rsr/LANGUAGE-POLICY.adoc b/0-canon/rsr/LANGUAGE-POLICY.adoc index 41b25393e..b7c74f5b0 100644 --- a/0-canon/rsr/LANGUAGE-POLICY.adoc +++ b/0-canon/rsr/LANGUAGE-POLICY.adoc @@ -3,7 +3,7 @@ = RSR Language Policy :author: Jonathan D.A. Jewell (hyperpolymath) -:revnumber: 1.6.0 +:revnumber: 1.7.0 :revdate: 2026-09-22 :toc: left :icons: font @@ -11,8 +11,11 @@ [NOTE] ==== -*Version Status*: v1.6.0 — corrects the inverted JavaScript-ecosystem policy: -Bun is tier 1 and Deno is banned (owner ruling 2026-09-22). This document had +*Version Status*: v1.7.0 — Deno is permitted again as tier 2 (owner ruling +2026-10-14): the order is Bun > Deno > pnpm > npm, with one `package.json` for +every runtime (`tools/js-runtimes`). v1.6.0 corrected the inverted +JavaScript-ecosystem policy: Bun is tier 1 and Deno was banned (owner ruling +2026-09-22). This document had frozen the 2026-04-10 order in which Deno was first and *Bun was banned*. v1.5.0 retired the telegram-bot, `affinescript-deno-test/` and `tsconfig.json` carve-outs; reframes `affinescript-cli/` as the permanent @@ -74,8 +77,14 @@ NOTE: For full rationale and migration guides, see the link:{source-repo}[CCCP r | JS runtime & package management (tier 1) | Default for all new JavaScript-ecosystem work; `bunx ` for one-off tooling. Uses an npm-compatible `package.json` plus `bun.lock` — both are - expected, not anti-patterns. Replaces Node/npm/Deno. Deno occupied this - row until 2026-09-22 and is now banned outright — see §Banned Languages. + expected, not anti-patterns. Replaces Node. Deno and pnpm read the same + `package.json`; `tools/js-runtimes` keeps all their locks in step. + +| *Deno* +| JS runtime (tier 2) +| Permitted, not gated (owner ruling 2026-10-14). Second in the order Bun > + Deno > pnpm > npm. Banned from 2026-09-22 to 2026-10-14; see §Amendments + v1.7.0. | *Gleam* | Backend services @@ -412,27 +421,29 @@ When encountering banned languages: == JavaScript / Node Ecosystem Policy Ratified 2026-04-10; reach order *superseded 2026-09-22* (Bun replaces Deno at -the head, and Deno is banned). When the work involves the JavaScript or Node -ecosystem, the reach order is: +the head) and *2026-10-14* (Deno returns as second; owner ruling). When the +work involves the JavaScript or Node ecosystem, the reach order is: 1. **Bun first, always.** Bun is npm-compatible, so it covers forced-Node package needs without actually having a Node project. Use `bunx ` for one-off tooling. -2. **pnpm as the Node-runtime fallback.** When a project *must* be +2. **Deno second.** Permitted, not gated. It reads the same `package.json` + (`deno task` runs its scripts), so a Bun package is also a Deno package; + every package carries a generated `deno.lock`. + +3. **pnpm as the Node-runtime fallback.** When a project *must* be Node-runtime (legacy integration, deployment target demands it), use **pnpm**. Rationale: pnpm's strict `node_modules` (no phantom deps) is a real security improvement over npm, its content-addressable store saves disk, its workspaces are sane, and it's compatible with the npm registry. -3. **npm last.** Tier 3 — permitted, never preferred; reaching for it is a - noted decision, not a default. `package-lock.json` must not be tracked. +4. **npm last.** Tier 4 — the last resort, only when nothing above can be had; + permitted, never preferred. `package-lock.json` must not be tracked. -4. **No other JavaScript runtimes or package managers.** Explicitly banned: +5. **No other JavaScript runtimes or package managers.** Explicitly banned: * `yarn` (classic + berry) — classic is abandoned, berry/PnP has compatibility pain - * `Deno` — banned 2026-09-22 by owner ruling (see §Banned Languages). - Migrate Deno to Bun; never the reverse. * `Node.js` as a standalone runtime — replaced by Bun == Interface & Architecture Law (NORMATIVE) @@ -582,6 +593,27 @@ not prose alone. == Amendments +=== v1.7.0 — 2026-10-14 (Jonathan D.A. Jewell) + +Deno is permitted again, as tier 2. The owner: "deno does not need to be +absolutely banned" and "I guess it would be bun, deno, pnpm, npm", confirmed +when asked. The reach order is Bun > Deno > pnpm > npm, with npm only as the +last resort. + +One `package.json` serves every runtime. `tools/js-runtimes` (Rust; JCS via +`ijson-jcs`) regenerates `bun.lock`, `deno.lock` and `pnpm-lock.yaml`, checks +each, picks the most-preferred runtime available, and checks that each +repository's environment provisions Bun. + +The Deno refusal gate and the `deno-allow.txt` ledger are retired. Sites +corrected: + +* §Allowed Languages: the Bun row, and a new Deno row. +* §JavaScript / Node Ecosystem Policy: Deno is second, pnpm third, npm fourth, + and Deno is off the banned list. + +Authoritative statement: `3-practice/LANGUAGE-POLICY.adoc` §1.3–§1.4. + === v1.6.0 — 2026-09-22 (Jonathan D.A. Jewell) Corrected an inverted JavaScript-ecosystem policy. This document had frozen diff --git a/1-formats/a2ml/meta/.claude/CLAUDE.md b/1-formats/a2ml/meta/.claude/CLAUDE.md index b45054f01..f94ec1a13 100644 --- a/1-formats/a2ml/meta/.claude/CLAUDE.md +++ b/1-formats/a2ml/meta/.claude/CLAUDE.md @@ -22,6 +22,7 @@ in A2ML format (migrated from Guile Scheme on 2026-04-12): |---------------|----------|-------| | **ReScript** | Primary application code | Compiles to JS, type-safe | | **Bun** | JS runtime & package management (tier 1) | 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. | +| **Deno** | JS runtime (tier 2) | Permitted, not gated (owner ruling 2026-10-14; order Bun > Deno > pnpm > npm). See `3-practice/LANGUAGE-POLICY.adoc` §1. | | **Rust** | Performance-critical, systems, WASM | Preferred for CLI tools | | **Tauri 2.0+** | Mobile apps (iOS/Android) | Rust backend + web UI | | **Dioxus** | Mobile apps (native UI) | Pure Rust, React-like | @@ -39,7 +40,6 @@ in A2ML format (migrated from Guile Scheme on 2026-04-12): | Banned | Replacement | |--------|-------------| | TypeScript | ReScript | -| Deno | Bun | | Node.js | Bun | | npm | Bun | | ~~Bun~~ | — | Bun is TIER 1 as of 3-practice/LANGUAGE-POLICY.adoc §1 (2026-07-29). This row is retired. | diff --git a/3-practice/JSON-POLICY.adoc b/3-practice/JSON-POLICY.adoc index 588100e2a..79ecd8f01 100644 --- a/3-practice/JSON-POLICY.adoc +++ b/3-practice/JSON-POLICY.adoc @@ -113,8 +113,11 @@ would differ from another JCS implementation's. * *Bot-written files.* Some JSON is written by tools that rewrite it in their own layout: `package.json` (`bun add`), `.hypatia-baseline.json`, scanner - output. Converting those is undone on the next tool run. Whether they are - carved out, or the tools are changed, is open. + output. Converting those is undone on the next tool run. For `package.json` + the answer is RULED in LANGUAGE-POLICY §1.4 (2026-10-14): `js-runtimes regen` + (tools/js-runtimes) re-canonicalises it after the tool, and the governance + `js-runtimes` jobs fail a forgotten run. For the + rest, whether they are carved out or the tools are changed is open. * *Mass reformatting.* Convert a repository's JSON in a PR scoped to that repository, after checking each consumer reads it by parsing, not by line. Do not reformat estate-wide outside a PR that adds the gate. diff --git a/3-practice/LANGUAGE-POLICY.adoc b/3-practice/LANGUAGE-POLICY.adoc index 2d8131b2f..564b06c19 100644 --- a/3-practice/LANGUAGE-POLICY.adoc +++ b/3-practice/LANGUAGE-POLICY.adoc @@ -16,25 +16,29 @@ Changing a rule here changes what CI enforces; do not edit casually. == 1. JavaScript / TypeScript runtimes and package managers -Ordered preference. Reach for the first one that can do the job. +Ordered preference (owner ruling 2026-10-14, §1.3). Reach for the first one +that can do the job, and provision it through the repository's own +environment so that it *can* (§1.4). [cols="1,3,6",options="header"] |=== | Rank | Tool | When | 1 | *Bun* -| Default for all new work. Native TypeScript execution, no build step, - built-in test runner and bundler. +| Default for all new work. Built-in test runner and bundler. Its lock, + `bun.lock`, is the one governance requires. -| 2 | *pnpm* -| Second. Only where an upstream toolchain requires a node_modules layout. +| 2 | *Deno* +| Second. Permitted, not gated (§1.3). Reads the same `package.json`, so in the + estate model it is a generated compatibility target of every package (§1.4). -| 3 | *npm* -| Last resort. Permitted, never preferred. Reaching for npm should be a - deliberate, noted decision — not a default. +| 3 | *pnpm* +| Third. Where an upstream toolchain requires a node_modules layout. Also a + generated compatibility target of every package (§1.4). -| — | *Deno* -| *BANNED.* Removed from the ordering 2026-09-22 — see §1.3. +| 4 | *npm* +| Last resort, only when nothing above can be had. Permitted, never preferred; + it keeps no tracked lock (`package-lock.json` stays refused, standards#67). |=== === 1.1 What this replaced @@ -95,51 +99,92 @@ deliberately planted violation. Not to withdraw the policy. === 1.3 Deno -RULED 2026-09-22 by the owner. *Deno is banned.* The ruling, verbatim: +RULED 2026-10-14 by the owner. *Deno is not banned.* The ruling, verbatim +(three messages, one direction): [quote] ____ -deno is over, we're prioritising bun, and using bunx +deno does not need to be absolutely banned, just bun is preferred and pnpm is +higher too + +ideally i do not want npm to be used but if that's the only option, even that +is okay with me + +basically ideally in all cases bun, then pnpm/deno, finally npm. I Want all to +have it but ideally do it via a rust thing, so I guess it would be bun, deno, +pnpm, npm ____ -This *supersedes* the grandfathering clause that stood in the §1 table from -2026-07-29 to 2026-09-22 ("Existing Deno projects are grandfathered and need -not migrate; prefer it over pnpm/npm when Bun genuinely cannot be used"). -Deno now holds no position in the ordering at all: where Bun cannot be used, -the fallback is *pnpm*, then npm. - -==== How this is enforced - -Not by prose. Three gates, and each prints its denominator: - -`.github/workflows/ci-pipeline.yml`, job `deno`:: A repository that tracks -`deno.json` or `deno.jsonc` goes *RED* unless its `owner/name` slug appears in -`.machine_readable/deno-allow.txt` in this repository. Until 2026-09-22 this -job installed a pinned Deno binary and ran `deno lint` / `deno fmt --check` -behind a `::warning::` — and a `::warning::` cannot fail a job, so it -lint-checked, and thereby blessed, the very thing it called banned. - -`.machine_readable/deno-allow.txt`:: A *shrink-only* exemption ledger, seeded -2026-09-22 from a complete 458-repository census of both accounts' default -branches: *11 repositories* still carry a Deno config. Registered in -`LEDGERS=()` in `scripts/check-exemption-ratchet.sh`, so adding a line needs a -`Ratchet-exception: .machine_readable/deno-allow.txt — ` commit trailer. -Exempt is not approved: it is measured, named, countable debt. - -`.githooks/validate-lint-format.sh`:: Refuses any commit that *adds or -modifies* a `deno.json`/`deno.jsonc`. It deliberately asks a narrower question -than CI, because it runs in a caller's checkout and cannot read the central -ledger — and a hook that guessed at an exemption would be worse than one that -admits its scope. Deletions are never blocked, so migrating away is never -obstructed. - -==== What migration actually costs - -The config file is the easy part. `bun run` does *not* read a `deno.json` -`tasks` map; those task definitions must be ported to `package.json` scripts. -Measured 2026-09-22 across the 23 tracked config files in the 11 ledgered -repositories: *95* task definitions to port. That is the substantive work, and -the reason this is a ledger rather than a flag day. +Asked to choose between "Bun > Deno > pnpm > npm" and "Bun > pnpm > Deno > +npm", the owner chose the first. Asked how the gates should treat a +repository that authors in Deno (tracks a `deno.json`), the owner chose +"notice only, retire the ledger". + +This *supersedes* the ban of 2026-09-22 ("deno is over, we're prioritising +bun, and using bunx"), which removed Deno from the ordering and enforced its +absence with a refusal job, a pre-commit refusal and a shrink-only ledger of +11 repositories. All three are retired: + +* `.github/workflows/ci-pipeline.yml`, job `deno`, reports the Deno configs a + repository tracks, named and counted, and passes. +* `.githooks/validate-lint-format.sh` records a staged `deno.json(c)` and + passes. +* `.machine_readable/deno-allow.txt` is deleted, and the exemption ratchet no + longer tracks it. + +`tools/policy/check-language-policy.sh` now treats a Deno row under a BANNED +heading as drift. + +What the 2026-09-22 ruling measured still holds as engineering fact. `bun run` +does not read a `deno.json` `tasks` map, so a repository that wants Bun first +keeps its scripts in `package.json`, which Deno 2 also runs (`deno task`). + +=== 1.4 One manifest, every runtime + +RULED 2026-10-14, on the owner's direction. The direction: "a form of +generative thing that always produces bun, deno, and pnpm outputs … the +problem of more than one was making sure it was always working and up to +date"; "I want all to have it but ideally do it via a rust thing"; and, +asked whether to build it, "yes but ensure this is ijson-jcs". The decision +record is `docs/decisions/2026-10-14-one-manifest-every-runtime.adoc`. + +`package.json` is the only manifest. Bun, Deno 2 and pnpm all read it +natively, so nothing is translated and no second manifest can drift. Each +runtime's own tool writes its own lockfile: `bun.lock` (tier 1, required), +then `deno.lock` and `pnpm-lock.yaml` (generated compatibility locks, never +hand-edited). npm keeps none. + +The tool is `tools/js-runtimes` (Rust; JCS in-process via the `ijson-jcs` +crate). + +Writer:: `js-runtimes regen` is the only writer. It makes `package.json` +I-JSON + JCS (D306) and regenerates all three locks. Run it after every +`bun add` / `pnpm add`. +Gate:: `.github/workflows/js-runtimes-reusable.yml`, and the same jobs in +`governance-reusable.yml`, run `js-runtimes check bun|deno|pnpm` on every +repository with a `package.json`. Each fails on: ++ +* a non-canonical `package.json`; +* a missing lock; +* a stale lock (the frozen install refuses it); +* a failing `test` script under that runtime's runner. ++ +`scripts/tests/js-runtimes-test.sh` proves each failure. +Order:: `js-runtimes pick` names the most-preferred runtime on `PATH`, and +`js-runtimes run SCRIPT` runs a script with it, so scripts and tooling +follow Bun > Deno > pnpm > npm wherever the environment allows. +Provisioning:: `js-runtimes provision` requires every environment definition +the repository carries to provision Bun: `mise.toml` (`[tools] bun = "…"`, +with checksums in `mise.lock`), each `Containerfile` (a Bun image, or a `RUN` +that installs Bun) and each Guix manifest. mise and containers can always +provide Bun, so settling for less there is a finding. Whether a Guix channel +packages Bun cannot be known from the repository, so for Guix the best runtime +it provisions is accepted and reported. A `package.json` that no environment +definition provisions a runtime for fails. + +Test scripts must be runtime-neutral. A `test` script that calls one runtime +by name may fail when that runtime is unavailable, and when it is available it +tests only that runtime, whatever the matrix leg. == 2. Package management @@ -210,13 +255,13 @@ Not permitted for new work anywhere in the estate: Banned *runtimes* are listed separately because the ban is on the runtime, not the language it executes: -* Deno — banned 2026-09-22, see §1.3. JavaScript itself is not banned; it runs - on Bun. +* None. Deno was banned from 2026-09-22 to 2026-10-14 (§1.3); JavaScript runs + on Bun first, then Deno, pnpm, npm (§1). CAUTION: Bans must be enforced by gates that can actually fail. Several "blocker" workflows in this estate have been structurally incapable of failing — -see §1.2, and §1.3 for a worked example: a job that announced a ban with a -`::warning::`, which cannot fail a job, while lint-checking the banned thing. When adding a gate, verify it *both* ways: that it passes on clean +see §1.2. A worked example: before 2026-09-22 the Deno job announced a ban with +a `::warning::`, which cannot fail a job, while lint-checking the banned thing. When adding a gate, verify it *both* ways: that it passes on clean input *and* fails on a deliberately planted violation. == 4. Amending this document diff --git a/3-practice/immaculate-guide/IMMACULATE-GUIDE.adoc b/3-practice/immaculate-guide/IMMACULATE-GUIDE.adoc index 98045608a..0a5ee96b9 100644 --- a/3-practice/immaculate-guide/IMMACULATE-GUIDE.adoc +++ b/3-practice/immaculate-guide/IMMACULATE-GUIDE.adoc @@ -112,7 +112,7 @@ with a pinned version. No undeclared toolchain dependencies. === Bun as Primary JS/TS Runtime -The JS runtime order is Bun > pnpm > npm; Deno is banned (2026-09-22). +The JS runtime order is Bun > Deno > pnpm > npm, npm only as the last resort (owner ruling 2026-10-14; Deno was banned 2026-09-22 to 2026-10-14). Declare JS dependencies in `package.json` and lock them with `bun.lock`. No `package-lock.json` (Hypatia `cicd_rules/nodejs_detected`, RSR 5.1.6). No `node_modules` in production. The authoritative ordering and its diff --git a/docs/JS-RUNTIME-POLICY.adoc b/docs/JS-RUNTIME-POLICY.adoc index 8eb808e45..d391c6164 100644 --- a/docs/JS-RUNTIME-POLICY.adoc +++ b/docs/JS-RUNTIME-POLICY.adoc @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 = JS Runtime & Package Management Policy -:revdate: 2026-05-31 +:revdate: 2026-10-14 :status: ACTIVE :issue-67: hyperpolymath/standards#67 :issue-68: hyperpolymath/standards#68 @@ -37,25 +37,38 @@ Tool selection MUST follow this hierarchy: | 1 | *Bun* -| Default for all new work. Executes TypeScript directly with no build step; -built-in test runner and bundler. +| Default for all new work. Built-in test runner and bundler; `bun.lock` is +the lock governance requires. | 2 -| *pnpm* -| Second. Only where an upstream toolchain requires a `node_modules` layout. +| *Deno* +| Second. Permitted, not gated (owner ruling 2026-10-14). Reads the same +`package.json`; every package carries a generated `deno.lock`. | 3 -| *npm* -| Last resort. Permitted, never preferred. Reaching for npm should be a -deliberate, noted decision — not a default. +| *pnpm* +| Third. Where an upstream toolchain requires a `node_modules` layout; every +package carries a generated `pnpm-lock.yaml`. -| — -| *Deno* -| *BANNED* 2026-09-22. Holds no position in the ordering. +| 4 +| *npm* +| Last resort, only when nothing above can be had. Permitted, never +preferred; keeps no tracked lock. |=== +`tools/js-runtimes` keeps one `package.json` (JCS) and the three locks in +step, checks each runtime, picks the most-preferred runtime available, and +checks that each repository's environment provisions Bun +(`3-practice/LANGUAGE-POLICY.adoc` §1.4). + [IMPORTANT] ==== +*Ruled 2026-10-14.* Deno is permitted again, as tier 2: the order is +Bun > Deno > pnpm > npm. The owner: "deno does not need to be absolutely banned". The +Deno ban recorded below (2026-09-22), its refusal gate and the +`deno-allow.txt` ledger are superseded and retired. +`3-practice/LANGUAGE-POLICY.adoc` §1.3 is authoritative. + *Corrected 2026-08-07.* This table previously ran `Deno > Bun > pnpm > npm`, the exact inverse of `3-practice/LANGUAGE-POLICY.adoc` §1, which has been the ruling since 2026-07-29. The ordering above now matches it. `3-practice/LANGUAGE-POLICY.adoc` is diff --git a/docs/decisions/2026-10-14-one-manifest-every-runtime.adoc b/docs/decisions/2026-10-14-one-manifest-every-runtime.adoc new file mode 100644 index 000000000..b4ec145e2 --- /dev/null +++ b/docs/decisions/2026-10-14-one-manifest-every-runtime.adoc @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MPL-2.0 += Decision – One package.json, every JavaScript runtime +:date: 2026-10-14 +:author: Jonathan D.A. Jewell (hyperpolymath) +:status: ACCEPTED (owner, 2026-10-14) +:scope: estate + +== Context + +The owner, 2026-10-14, verbatim, in order: + +[quote] +____ +bun is the main one from now on. but I wonder, is there a way of estate-wide +making it so that there is a form of generative thing that always produces +bun, deno, and pnpm outputs for these things - the problem of more than one +was making sure it was always working and up to date - to do that opens up +all three options I would be happy with rather than forcing one +____ + +Asked whether to build it: "yes but ensure this is ijson-jcs". That means +D306: every `.json` is I-JSON, serialised by JCS. + +Then, while it was being built: + +[quote] +____ +deno does not need to be absolutely banned, just bun is preferred and pnpm is +higher too + +ideally i do not want npm to be used but if that's the only option, even that +is okay with me + +basically ideally in all cases bun, then pnpm/deno, finally npm. I Want all to +have it but ideally do it via a rust thing, so I guess it would be bun, deno, +pnpm, npm - my major worry as I say is constantly updating all of them to keep +in sync and making sure that the preference order is satisfied if at all +possible (including bunx or bun distribution as part of the environment/repo + +yes and that for everything that requires one of these things to operate, +built into the repo/environment set up (e.g. containers, guix, mise, etc. etc.) +____ + +The owner's answers to the choices put to them (selector, 2026-10-14): + +[cols="2,3",options="header"] +|=== +| Question | Answer +| Order | Bun > Deno > pnpm > npm +| A repository that authors in Deno (`deno.json`) | Notice only; retire the refusal ledger +| Where the Rust tool lives | A crate in `standards/tools/`, beside `tools/uuid-v8` +| How Bun is guaranteed | `mise.toml` pins, with `mise.lock` checksums +| Opt-in or governance | Join governance now +| Ruling text | Ratify (RULED, not PROPOSED) +|=== + +== Decision + +*One manifest, every runtime.* + +- `package.json` is the only manifest. Bun, Deno 2 and pnpm all read it +natively: dependencies through their installers, scripts through `bun run`, +`deno task` and `pnpm run`. Nothing is translated, so no second manifest can +drift. +- Each runtime that keeps a lock writes its own: `bun.lock` (tier 1), +`deno.lock` and `pnpm-lock.yaml`. +- npm, the last resort, keeps none (`package-lock.json` stays refused, +standards#67). + +*The tool* is `tools/js-runtimes` (Rust). It uses the `ijson-jcs` crate +in-process at a pinned rev. + +[cols="1,3",options="header"] +|=== +| Command | What it does +| `regen` | The only writer. Makes `package.json` JCS, then regenerates `bun.lock`, `deno.lock` and `pnpm-lock.yaml`, each with its own tool, then re-canonicalises. +| `check bun\|deno\|pnpm` | The gate. Checks JCS, that the lock is present, a frozen install (a stale lock fails), and the `test` script under that runtime's runner. +| `pick` / `run SCRIPT` | The most-preferred runtime on `PATH`, and a `package.json` script run with it. Scripts and tooling follow the order wherever the environment allows. +| `provision` | Every environment definition the repository carries (`mise.toml`, each `Containerfile`, each Guix manifest) provisions Bun. mise and containers can always provide Bun, so settling for less there is a finding. For Guix, the best runtime it provisions is accepted and reported. A `package.json` that no environment definition provisions a runtime for fails. +|=== + +*The gate* runs in `governance-reusable.yml`: a provision job, and one check +job each for Bun, Deno and pnpm. The same jobs are offered standalone in +`js-runtimes-reusable.yml`. A repository without a `package.json` passes with +a notice naming the denominator (0). + +*Deno* is tier 2 and is not gated (§1.3 of LANGUAGE-POLICY). The refusal job, +the pre-commit refusal and the `deno-allow.txt` ledger are retired. + +== Consequences + +* *JSON-POLICY §6.* It left `package.json` open as a bot-written file: +`bun add` rewrites it in its own layout, undoing JCS. `regen` after +`bun add` / `pnpm add` puts it back, and the gate catches a forgotten run. +* *Once callers bump their governance pin*, every repository with a +`package.json` must carry all three locks and a JCS `package.json`. +Repositories that don't will go red until `js-runtimes regen` is run and +committed, and until their environment definitions pin Bun. The owner chose +this knowingly ("Join governance now"). +* *Test scripts must be runtime-neutral.* Each check job installs only its own +runtime, so a `"test": "bun test"` fails the Deno and pnpm legs where Bun is +absent. Where Bun is present, it tests Bun, whatever the leg. +* *Deno does not record `file:` dependencies in `deno.lock`.* A stale `file:` +dependency is caught by the Bun and pnpm legs only (measured 2026-10-14, +Deno 2.9.6). Registry dependencies are caught on all three legs. +* *Pins.* +** Bun 1.4.2 (`oven-sh/setup-bun` v2.2.0) and Deno 2.9.6 (`denoland/setup-deno` +v2.0.5), both SHA-pinned setup actions already in estate locks. +** pnpm 10.28.0, via Corepack pinned by its tarball's sha512. A wrong hash is +refused. +** ijson-jcs: the crate at `3a9a4ee`, and the CLI at `0fdeea71` for the +self-test. +** The pins appear in `js-runtimes-reusable.yml`, `governance-reusable.yml` and +`self-test.yml`, and are bumped together. +* *An earlier draft* installed the runtimes with `npm install -g`. Scorecard +then raised four medium alerts, one per install line, and SonarCloud's +new-code security rating fell to C. Its finding could not be read from the +agent sandbox. The pinned setup replaced it. + +== Follow-ups + +* *rsr-template-repo:* pin Bun (and Deno and pnpm, for their legs) in the +template's `mise.toml`, Containerfile and Guix templates, so every newly minted +repository satisfies `js-runtimes provision`. +* *Callers* bump their governance pin to a commit that carries these jobs. diff --git a/scripts/check-exemption-ratchet.sh b/scripts/check-exemption-ratchet.sh index cda3cc31e..cf05244da 100755 --- a/scripts/check-exemption-ratchet.sh +++ b/scripts/check-exemption-ratchet.sh @@ -148,7 +148,6 @@ LEDGERS=( ".gitleaks.toml" ".machine_readable/root-allow.txt" ".machine_readable/lock-allow.txt" - ".machine_readable/deno-allow.txt" ".machine_readable/pipeline-allow.txt" ) diff --git a/scripts/tests/detect-ecosystem-test.sh b/scripts/tests/detect-ecosystem-test.sh index 1c75c01fc..4de60c677 100755 --- a/scripts/tests/detect-ecosystem-test.sh +++ b/scripts/tests/detect-ecosystem-test.sh @@ -139,7 +139,7 @@ check "names Bun" "$(grep -qF 'Bun (2)' <<< "$OUT" && echo y || e check "refuses (verdict)" "$R" "true" check "job still exits 0" "$RC" "0" -echo "== Fixture 6: deno.json still trips the BAN probe ==" +echo "== Fixture 6: deno.json trips the Deno probe (tier 2: reported, not refused) ==" run_fixture true '' deno.json check "has_deno true" "$(grep -qF 'has_deno=true' <<< "$OUT" && echo y || echo n)" "y" check "n_deno" "$(sed -n 's/^n_deno=//p' <<< "$OUT" | tail -1)" "1" diff --git a/scripts/tests/js-runtimes-test.sh b/scripts/tests/js-runtimes-test.sh new file mode 100755 index 000000000..b495bb24a --- /dev/null +++ b/scripts/tests/js-runtimes-test.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Proves tools/js-runtimes CAN FAIL, for the right reason: +# * check, under each of bun, deno and pnpm: a regenerated package passes; a +# non-canonical package.json, a missing lock, a stale lock and a failing +# test script each fail and say why; +# * pick / run follow the preference order Bun > Deno > pnpm > npm; +# * provision accepts an environment that provisions Bun, refuses one that +# settles for less where Bun is always possible, and reports a Guix +# fallback; +# * a missing tool, an unknown runtime and `check npm` are exit 2 ("cannot +# run"), never a pass. +# +# Builds the tool with cargo unless $JS_RUNTIMES names a binary. Needs bun, +# deno and pnpm, and the npm registry: the fixture depends on two tiny +# zero-dependency packages, because deno.lock does not record file: +# dependencies, so an offline fixture could never show a stale deno.lock. +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +for t in bun pnpm deno cargo; do + command -v "$t" >/dev/null 2>&1 || { echo "js-runtimes-test: $t is not on PATH (cannot run, not a pass)" >&2; exit 2; } +done +if [ -z "${JS_RUNTIMES:-}" ]; then + unit_log="$(mktemp)" + cargo test --quiet --locked --manifest-path "$ROOT/tools/js-runtimes/Cargo.toml" >"$unit_log" 2>&1 \ + || { cat "$unit_log"; rm -f "$unit_log"; echo "js-runtimes-test: the unit tests failed" >&2; exit 1; } + rm -f "$unit_log" + echo " ok unit tests (cargo test)" +fi +BIN="${JS_RUNTIMES:-}" +if [ -z "$BIN" ]; then + cargo build --quiet --release --locked --manifest-path "$ROOT/tools/js-runtimes/Cargo.toml" \ + || { echo "js-runtimes-test: the tool did not build" >&2; exit 2; } + BIN="$ROOT/tools/js-runtimes/target/release/js-runtimes" +fi +WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT + +pass=0; fail=0 +expect() { # expect