Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/governance-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,36 @@ jobs:
exit 1
fi
echo "✅ Security policy check passed"
- name: Tooling version integrity
# Estate Tooling Version Integrity policy (root cause: burble#39).
# Inline + dependency-free so it runs in any caller repo.
# R0 just>=1.19.0 floor (blocking when just present) and R1
# unversioned family-tool install (blocking) are hard; R4
# unexplained continue-on-error is advisory-first per the
# documented "advisory now, --strict later" gating doctrine.
run: |
set -uo pipefail
FAMILY='just|must|trust|adjust|bust|dust|intend'
if command -v just >/dev/null 2>&1; then
jv=$(just --version 2>/dev/null | cut -d' ' -f2)
maj=${jv%%.*}; rest=${jv#*.}; min=${rest%%.*}
if [ -z "$jv" ] || ! { [ "${maj:-0}" -gt 1 ] || { [ "${maj:-0}" -eq 1 ] && [ "${min:-0}" -ge 19 ]; }; }; then
echo "❌ [R0] just ${jv:-?} < 1.19.0 — import? unsupported"; exit 1
fi
echo "✅ [R0] just $jv >= 1.19.0"
else
echo "ℹ️ [R0] just not on PATH — skipped"
fi
R1=0
if [ -d .github/workflows ]; then
while IFS= read -r hit; do
[ -n "$hit" ] || continue
echo "❌ [R1] unversioned family-tool install: $hit"
R1=$((R1+1))
done < <(grep -rnE "^[[:space:]]*tool:[[:space:]]*(${FAMILY})[[:space:]]*$" .github/workflows 2>/dev/null || true)
fi
[ "$R1" -gt 0 ] && { echo "❌ [R1] $R1 unversioned family-tool install(s) — pin tool: <name>@<ver>"; exit 1; }
echo "✅ Tooling version integrity passed (R1 clean; R4 advisory via standards/tasks/tooling-integrity-lint.sh)"

quality:
name: Code quality + docs
Expand All @@ -258,6 +288,10 @@ jobs:
path: ./
base: ${{ github.event.pull_request.base.sha || github.event.before }}
head: ${{ github.sha }}
# by-design: trufflehog is a best-effort advisory scan; a scanner
# diff/range hiccup must not fail the whole governance gate. The
# blocking secret check is the inline grep in the security job.
# (Tooling Version Integrity Rule 4 — documented soft-gate.)
continue-on-error: true
- name: Check TODO/FIXME
run: |
Expand Down
5 changes: 5 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# SPDX-License-Identifier: PMPL-1.0-or-later
# justfile - Just recipes for this project
# See: https://github.com/hyperpolymath/mustfile
#
# requires: just >= 1.19.0 (import? optional-import support)
# Enforced by the `tooling-version-integrity` must-check, not self-
# enforcing: import? fails at parse time before any recipe can guard it.
# See TOOLING-VERSION-INTEGRITY-POLICY.adoc (root cause: burble#39).

# Default recipe
import? "contractile.just"
Expand Down
2 changes: 2 additions & 0 deletions Mustfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ checks:
run: just test
- name: format
run: just fmt
- name: tooling-version-integrity
run: bash tasks/tooling-integrity-lint.sh
144 changes: 144 additions & 0 deletions TOOLING-VERSION-INTEGRITY-POLICY.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
= Hyperpolymath Tooling Version Integrity Policy
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
:toc:
:toc-placement: preamble

Canonical policy for how the rhyming command-runner family
(`just`/Justfile, `must`/Mustfile, `trust`/Trustfile, `adjust`/Adjustfile,
`bust`/Bustfile, `dust`/Dustfile, `intend`/Intentfile) and their
`contractile`-generated fragments are versioned, installed, and gated
across all hyperpolymath repositories. All contributors and AI agents must
follow this document.

== Why this policy exists (the burble#39 post-mortem)

For months a burble Elixir test gate was bolted open
(`continue-on-error: true`) on the belief that "the suite is red for a
pre-existing reason". It was not. CI installed `just` *unversioned* via an
install-action's bundled manifest, which shipped `just` 1.14.0. The
Justfile used `import?` (optional import, requires `just >= 1.19.0`), so
`just` died at *parse time* with `error: Unknown start of token` —
**before any recipe ran**. `mix test --no-start` never executed. A
runner-layer crash was misdiagnosed for months as an inner-layer test
failure, and a suppressed signal froze the wrong diagnosis in place.

Root causes, generalised:

. *Silent transitive version skew* — nobody chose 1.14.0; a bundled
manifest did, invisibly.
. *Feature/version coupling with no declared floor* — `import?` needs
`just >= 1.19.0`; nothing declared or enforced that minimum.
. *Outer-layer failure misattributed to an inner layer* — `just` crashed
before the tests; the redness was labelled "failing tests".
. *A suppressed signal froze a wrong diagnosis* — `continue-on-error`
added on an unverified root cause, hiding the real state indefinitely.

The transferable invariant: **a green/red bit cannot distinguish "passed"
from "never ran".** Every gate is blind to this unless it proves the work
actually happened.

== Rule 1 — Never install a family tool unversioned

Any workflow step that installs a rhyming-family tool MUST pin an explicit
version that satisfies every feature the consumed files use.

[source,yaml]
----
# WRONG — resolves via the action's bundled manifest (may be ancient)
- uses: taiki-e/install-action@<sha> # vX
with:
tool: just

# RIGHT — explicit, satisfies `import?` (>= 1.19.0)
- uses: taiki-e/install-action@<sha> # vX
with:
tool: just@1.34.0
----

`just@1.34.0` is the current estate-canonical pin. Bump it centrally via
the canonical CI templates (`rsr-template-repo` → `v3-templater` /
`reposystem`), never per-repo.

== Rule 2 — Declare the minimum tool version in the file

Every `Justfile`/`Mustfile`/`Trustfile`/etc. MUST carry a machine-greppable
floor annotation near the top, immediately documenting the coupling:

----
# requires: just >= 1.19.0 (import? optional-import support)
----

This annotation is *documentation and a lint target*. It is **not
self-enforcing**: `import?` fails at parse time, before any recipe or
guard inside the file can run. The executable assertion lives in Rule 3.

== Rule 3 — Gates must prove execution, not exit 0

A "passed" check MUST assert that the underlying work actually ran, not
merely that nothing returned non-zero. Concretely:

* The canonical `must` contract carries a `tooling-version-integrity`
check (see `contractiles/must/Mustfile`) that asserts the installed
`just` satisfies the declared floor *and* runs the workflow lint. This
check runs after `just` is on `PATH`, so it catches the parse-time
class that an in-file guard structurally cannot.
* Test steps SHOULD emit a positive execution sentinel (e.g. a non-zero
test count or an explicit marker line); CI SHOULD fail on the
*absence* of that sentinel, which is what catches "the runner died
before the work".

== Rule 4 — Every soft-gate must be explained

`continue-on-error: true` (or any soft-gate) MUST be explained, in a
comment within the 12 lines above it, in exactly one of two ways:

. *Suppressed gate* — a known-failing gate temporarily bolted open. The
comment MUST contain `GATE DEACTIVATED <ISO-DATE>`, the *verified* root
cause (not a guess — burble#39's guess was wrong), and the explicit
single-line re-arm trigger. This form is debt and must carry a path
back to armed.
. *By-design advisory* — a step that is best-effort by nature (e.g.
resilience to an upstream outage, optional enrichment). The comment
MUST carry a `by-design:` or `advisory:` rationale. This form is not
debt; it documents intent so reviewers do not mistake it for a
suppressed gate.

A bare `continue-on-error` with neither is a policy violation, flagged by
`tasks/tooling-integrity-lint.sh` (rule R4).

=== Rollout: advisory first, tighten later

R4 ships *advisory* (reported, non-blocking) by default; R1 (unversioned
installs) is blocking immediately. This deliberately follows the same
"advisory first, tighten later" gating doctrine the estate adopted for
Hypatia SARIF (burble#35 item 3) — a new policy gate enforced as a
hard estate-wide failure on day one would itself be bolted open, exactly
the failure mode this policy exists to end. Promote R4 to blocking
(`--strict`) per repo as its existing soft-gates are explained.

== Rule 5 — Resolve at source, never per-repo

Fixes to any of the above are made in the canonical source
(`contractile` source under `contractiles/`, the canonical CI/Justfile
templates in `rsr-template-repo` → `v3-templater` / `reposystem`) and
propagated by re-adoption. Per-repo patches recreate the drift this
policy exists to prevent.

NOTE: The `contractile` generator itself is currently out-of-band (the
generator repository is not part of the working estate). Until it is
in-tree, changes to generated `contractile.just` content are made in the
`contractiles/` *source* fragments and regenerated by whoever holds the
generator; consumers must re-run `contractile gen-just --dir contractiles`
to pick them up. This gap is tracked estate-wide (see the Tooling Version
Integrity sweep tracking issue).

== Enforcement

* `tasks/tooling-integrity-lint.sh` — scans `.github/workflows/` for
unversioned family-tool installs (Rule 1) and bare `continue-on-error`
(Rule 4). Exit non-zero on any violation.
* `contractiles/must/Mustfile` (canonical template) and this repo's live
`Mustfile` carry a `tooling-version-integrity` mandatory check that
runs the lint — so every repo adopting the canonical `must` contract
inherits the gate (Rule 3, Rule 5).
4 changes: 4 additions & 0 deletions contractiles/must/Mustfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ checks:
- name: no-template-residue
description: "No placeholder text from the contractiles template must remain."
run: "bash -uc '! rg -rn \"REPLACE-WITH|PLMP-1.0-or-later\" --type-not binary . | rg .'"

- name: tooling-version-integrity
description: "Installed just must satisfy the import? floor (>= 1.19.0). Dependency-free; proves the running just is new enough — the burble#39 invariant an in-file guard cannot enforce. See standards TOOLING-VERSION-INTEGRITY-POLICY.adoc."
run: "bash -uc 'command -v just >/dev/null 2>&1 || exit 0; jv=$(just --version 2>/dev/null | cut -d\" \" -f2); test -n \"$jv\" || { echo \"just present, version unreadable\"; exit 1; }; maj=${jv%%.*}; rest=${jv#*.}; min=${rest%%.*}; { [ \"$maj\" -gt 1 ] || { [ \"$maj\" -eq 1 ] && [ \"$min\" -ge 19 ]; }; } || { echo \"just $jv < 1.19.0 import? unsupported\"; exit 1; }'"
124 changes: 124 additions & 0 deletions scripts/registry-readiness.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# registry-readiness.sh — make a Julia package honest for the General registry.
#
# Encodes the owner-approved remediation applied to Axiom.jl (#18) and
# AcceleratorGate.jl (#8): resolve the PMPL/MPL-2.0 dual-license
# contradiction to a single OSI licence (MPL-2.0) with REUSE compliance,
# remove LLM-tell scaffolding, and SANITY-CHECK (not auto-edit) the
# Project.toml for fabricated dependency UUIDs.
#
# Principles (the whole point — do not violate):
# * No fabrication. Ambiguous things are REPORTED, never invented.
# * Non-destructive beyond the known-safe set below.
# * Idempotent. Run it twice → same result.
# * Run it ON A BRANCH; it does not commit or push.
#
# Usage: cd <repo> && git switch -c cleanup/registry-readiness && \
# bash /path/to/standards/scripts/registry-readiness.sh
#
set -euo pipefail
RED=$'\033[31m'; GRN=$'\033[32m'; YEL=$'\033[33m'; NC=$'\033[0m'
say() { printf '%s\n' "$*"; }
ok() { printf '%s✓%s %s\n' "$GRN" "$NC" "$*"; }
warn(){ printf '%s!%s %s\n' "$YEL" "$NC" "$*"; }
flag(){ printf '%s⚠ NEEDS HUMAN%s %s\n' "$RED" "$NC" "$*"; }

[ -d .git ] || { echo "not a git repo"; exit 1; }
[ -f Project.toml ] || warn "no Project.toml — not a Julia package? continuing anyway"

say "== 1. Licensing → MPL-2.0 (REUSE) =="
if grep -rlq "PMPL-1.0-or-later" $(git ls-files) 2>/dev/null; then
# 1a. SPDX identifiers + the dual-license contradiction comment
git ls-files -z | xargs -0 grep -lZ "PMPL-1.0-or-later" 2>/dev/null | xargs -0 -r sed -i \
-e 's|SPDX-License-Identifier: PMPL-1.0-or-later|SPDX-License-Identifier: MPL-2.0|g' \
-e '/(PMPL-1\.0-or-later preferred; MPL-2\.0 required for Julia ecosystem)/d'
# 1b. canonical LICENSE from the bundled MPL text (do NOT fabricate licence text)
if [ -f LICENSES/MPL-2.0.txt ]; then
{ echo "SPDX-License-Identifier: MPL-2.0"
echo "SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>"
echo; cat LICENSES/MPL-2.0.txt; } > LICENSE
ok "LICENSE rebuilt from LICENSES/MPL-2.0.txt"
else
flag "LICENSES/MPL-2.0.txt absent — cannot rebuild LICENSE without fabricating licence text. Fix by hand."
fi
rm -f LICENSES/PMPL-1.0-or-later.txt; git rm -q --cached LICENSES/PMPL-1.0-or-later.txt 2>/dev/null || true
printf 'version = 1\n\n[[annotations]]\npath = "**"\nprecedence = "aggregate"\nSPDX-FileCopyrightText = "2024-2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>"\nSPDX-License-Identifier = "MPL-2.0"\n' > REUSE.toml
ok "SPDX headers → MPL-2.0; REUSE.toml written"
# 1c. broad SAFE prose normalisation — every one of these is a licence
# reference that must now read MPL-2.0; none changes meaning.
git ls-files -z | xargs -0 grep -lZ "PMPL\|Palimpsest" 2>/dev/null | xargs -0 -r sed -i \
-e 's/PMPL-1\.0-or-later/MPL-2.0/g' \
-e 's/Palimpsest License (MPL-2.0)/Mozilla Public License 2.0/g' \
-e 's/Palimpsest License/Mozilla Public License 2.0/g' \
-e 's/the Palimpsest/the Mozilla Public License 2.0/g' \
-e 's/Palimpsest/Mozilla Public License 2.0/g' \
-e 's#https://github.com/hyperpolymath/palimpsest-license#https://www.mozilla.org/MPL/2.0/#g' \
-e 's/License-PMPL--1\.0-blue/License-MPL--2.0-blue/g' \
-e 's/license = with licenses; \[ mit \];.*/license = with licenses; [ mpl20 ]; # MPL-2.0/' \
-e 's/"Add PMPL-1\.0 license"/"Add MPL-2.0 license"/g' \
-e "s/'SPDX\\\\|License\\\\|MIT\\\\|Apache\\\\|PMPL\\\\|MPL'/'SPDX\\\\|License\\\\|MIT\\\\|Apache\\\\|MPL'/g" \
-e "s/'PMPL\\\\|MPL\\\\|MIT\\\\|Apache\\\\|LGPL'/'MPL\\\\|MIT\\\\|Apache\\\\|LGPL'/g" \
-e '/preserve Emotional Lineage per PMPL/d' \
-e '/automatic legal fallback until PMPL is formally recognised/d' \
-e '/^.*\bPMPL\b.*Section [0-9].*$/d'
# 1c-bis. ROBUST SPDX normalisation. The broad prose pass can leave or
# create non-canonical identifiers (`MPL-2.0 +` from `... +` tails,
# pre-existing `MPL-2.0-or-later`, the `PLMP-1.0-or-later` typo). reuse
# lint rejects these. Force every SPDX-License-Identifier value to the
# single canonical `MPL-2.0` (this is what makes `reuse lint` pass).
git ls-files -z | xargs -0 -r sed -i -E \
-e 's/(SPDX-License-Identifier:) +MPL-2\.0 \+/\1 MPL-2.0/g' \
-e 's/(SPDX-License-Identifier:) +MPL-2\.0-or-later/\1 MPL-2.0/g' \
-e 's/(SPDX-License-Identifier:) +P[LM]MP-1\.0-or-later/\1 MPL-2.0/g'
# 1d. canonical NOTICE (standard MPL-2.0 notice — not fabrication, the
# generic form; replaces any bespoke PMPL paragraph deterministically)
if [ -f NOTICE ]; then
repo=$(basename "$(git rev-parse --show-toplevel)")
cat > NOTICE <<EOF
Licensing Notice
================

$repo is authored by Jonathan D.A. Jewell (hyperpolymath)
<j.d.a.jewell@open.ac.uk> and is licensed under the Mozilla Public
License 2.0 (MPL-2.0), an OSI-approved licence.

The full licence text is in LICENSE and LICENSES/MPL-2.0.txt. Per-file
SPDX-License-Identifier headers and REUSE.toml record MPL-2.0
consistently across the repository (REUSE-compliant).
EOF
ok "NOTICE rewritten to canonical MPL-2.0 form"
fi
resid=$(git ls-files | xargs grep -l "PMPL\|Palimpsest" 2>/dev/null | grep -v '\.git/' || true)
[ -n "$resid" ] && { warn "residual PMPL/Palimpsest refs still present:"; printf ' %s\n' $resid; } || ok "0 PMPL/Palimpsest refs remain"
[ -f README.adoc ] || [ -f README.md ] && flag "README: verify it has purpose + a run-verified usage example + prior-art (goerz/DilumAluthge asked) — content-specific, not auto-generated"
else
ok "no PMPL references"
fi
command -v reuse >/dev/null 2>&1 && { reuse lint >/dev/null 2>&1 && ok "reuse lint passes" || warn "reuse lint not clean — run 'reuse lint' and inspect"; } || warn "reuse CLI not installed (pipx install reuse)"

say "== 2. De-LLM the package face =="
for f in 0-AI-MANIFEST.a2ml llm-warmup-dev.md llm-warmup-user.md; do
[ -e "$f" ] && { git rm -q "$f" 2>/dev/null || rm -f "$f"; ok "removed $f"; }
done
[ -e EXPLAINME.adoc ] && flag "EXPLAINME.adoc present — do NOT auto-delete; rewrite to verified truth or remove by decision"

say "== 3. Project.toml manifest sanity (REPORT ONLY — never auto-edit deps) =="
if [ -f Project.toml ]; then
if grep -qE '"([0-9])\1{7}-' Project.toml; then
flag "fabricated sequential-dummy weakdep UUIDs in Project.toml — remove the phantom deps+extensions by hand (see Axiom.jl#18)"
else ok "no obvious fabricated UUIDs"; fi
grep -q '^license *= *"MPL-2.0"' Project.toml && ok 'Project.toml license = "MPL-2.0"' \
|| flag "Project.toml license is not MPL-2.0 — set it"
fi

say "== 4. Tests (authoritative — paste real result) =="
if [ -f Project.toml ] && command -v julia >/dev/null 2>&1; then
if timeout 560 julia --project=. -e 'using Pkg; Pkg.test()' 2>&1 | tee /tmp/rr_test.$$ | tail -3; then
grep -q "Testing .* tests passed" /tmp/rr_test.$$ && ok "Pkg.test() passed" || flag "Pkg.test() did NOT pass — inspect /tmp/rr_test.$$"
else flag "Pkg.test() errored — inspect /tmp/rr_test.$$"; fi
else warn "skipped tests (no Project.toml or julia)"; fi

say "${GRN}== registry-readiness pass complete — review, then commit on this branch ==${NC}"
Loading
Loading