Skip to content

Commit 7d6e728

Browse files
hyperpolymathclaude
andcommitted
policy: Tooling Version Integrity (burble#39 institutionalised)
Codifies the burble#39 lesson estate-wide so a runner-layer version skew can never again masquerade for months as an inner-layer failure. - TOOLING-VERSION-INTEGRITY-POLICY.adoc: 5 rules + post-mortem. Pin family tools; declare the min-version floor; gates prove execution not exit-0; every soft-gate explained (dated suppression OR by-design); resolve at source. - tasks/tooling-integrity-lint.sh: R0 just>=1.19.0 floor (blocking when just present — the execution-proof check an in-file guard cannot do), R1 unversioned family-tool install (blocking), R4 unexplained continue-on-error (advisory-first per the estate gating doctrine; --strict to enforce). Self-tested vs standards/burble/hypatia. - contractiles/must/Mustfile: canonical 'tooling-version-integrity' check (dependency-free inline floor assertion) — propagates to every repo adopting the canonical must contract on contractile regen. - Mustfile: live repo dogfoods the full lint. - Justfile: Rule-2 'requires: just >= 1.19.0' annotation. NOTE: the contractile generator repo is out-of-band; contractiles/ source is edited here, regen + propagation tracked in the estate sweep issue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fa1caa4 commit 7d6e728

5 files changed

Lines changed: 246 additions & 0 deletions

File tree

Justfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# SPDX-License-Identifier: PMPL-1.0-or-later
22
# justfile - Just recipes for this project
33
# See: https://github.com/hyperpolymath/mustfile
4+
#
5+
# requires: just >= 1.19.0 (import? optional-import support)
6+
# Enforced by the `tooling-version-integrity` must-check, not self-
7+
# enforcing: import? fails at parse time before any recipe can guard it.
8+
# See TOOLING-VERSION-INTEGRITY-POLICY.adoc (root cause: burble#39).
49

510
# Default recipe
611
import? "contractile.just"

Mustfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ checks:
1111
run: just test
1212
- name: format
1313
run: just fmt
14+
- name: tooling-version-integrity
15+
run: bash tasks/tooling-integrity-lint.sh
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
= Hyperpolymath Tooling Version Integrity Policy
3+
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
:toc:
5+
:toc-placement: preamble
6+
7+
Canonical policy for how the rhyming command-runner family
8+
(`just`/Justfile, `must`/Mustfile, `trust`/Trustfile, `adjust`/Adjustfile,
9+
`bust`/Bustfile, `dust`/Dustfile, `intend`/Intentfile) and their
10+
`contractile`-generated fragments are versioned, installed, and gated
11+
across all hyperpolymath repositories. All contributors and AI agents must
12+
follow this document.
13+
14+
== Why this policy exists (the burble#39 post-mortem)
15+
16+
For months a burble Elixir test gate was bolted open
17+
(`continue-on-error: true`) on the belief that "the suite is red for a
18+
pre-existing reason". It was not. CI installed `just` *unversioned* via an
19+
install-action's bundled manifest, which shipped `just` 1.14.0. The
20+
Justfile used `import?` (optional import, requires `just >= 1.19.0`), so
21+
`just` died at *parse time* with `error: Unknown start of token` —
22+
**before any recipe ran**. `mix test --no-start` never executed. A
23+
runner-layer crash was misdiagnosed for months as an inner-layer test
24+
failure, and a suppressed signal froze the wrong diagnosis in place.
25+
26+
Root causes, generalised:
27+
28+
. *Silent transitive version skew* — nobody chose 1.14.0; a bundled
29+
manifest did, invisibly.
30+
. *Feature/version coupling with no declared floor* — `import?` needs
31+
`just >= 1.19.0`; nothing declared or enforced that minimum.
32+
. *Outer-layer failure misattributed to an inner layer* — `just` crashed
33+
before the tests; the redness was labelled "failing tests".
34+
. *A suppressed signal froze a wrong diagnosis* — `continue-on-error`
35+
added on an unverified root cause, hiding the real state indefinitely.
36+
37+
The transferable invariant: **a green/red bit cannot distinguish "passed"
38+
from "never ran".** Every gate is blind to this unless it proves the work
39+
actually happened.
40+
41+
== Rule 1 — Never install a family tool unversioned
42+
43+
Any workflow step that installs a rhyming-family tool MUST pin an explicit
44+
version that satisfies every feature the consumed files use.
45+
46+
[source,yaml]
47+
----
48+
# WRONG — resolves via the action's bundled manifest (may be ancient)
49+
- uses: taiki-e/install-action@<sha> # vX
50+
with:
51+
tool: just
52+
53+
# RIGHT — explicit, satisfies `import?` (>= 1.19.0)
54+
- uses: taiki-e/install-action@<sha> # vX
55+
with:
56+
tool: just@1.34.0
57+
----
58+
59+
`just@1.34.0` is the current estate-canonical pin. Bump it centrally via
60+
the canonical CI templates (`rsr-template-repo` → `v3-templater` /
61+
`reposystem`), never per-repo.
62+
63+
== Rule 2 — Declare the minimum tool version in the file
64+
65+
Every `Justfile`/`Mustfile`/`Trustfile`/etc. MUST carry a machine-greppable
66+
floor annotation near the top, immediately documenting the coupling:
67+
68+
----
69+
# requires: just >= 1.19.0 (import? optional-import support)
70+
----
71+
72+
This annotation is *documentation and a lint target*. It is **not
73+
self-enforcing**: `import?` fails at parse time, before any recipe or
74+
guard inside the file can run. The executable assertion lives in Rule 3.
75+
76+
== Rule 3 — Gates must prove execution, not exit 0
77+
78+
A "passed" check MUST assert that the underlying work actually ran, not
79+
merely that nothing returned non-zero. Concretely:
80+
81+
* The canonical `must` contract carries a `tooling-version-integrity`
82+
check (see `contractiles/must/Mustfile`) that asserts the installed
83+
`just` satisfies the declared floor *and* runs the workflow lint. This
84+
check runs after `just` is on `PATH`, so it catches the parse-time
85+
class that an in-file guard structurally cannot.
86+
* Test steps SHOULD emit a positive execution sentinel (e.g. a non-zero
87+
test count or an explicit marker line); CI SHOULD fail on the
88+
*absence* of that sentinel, which is what catches "the runner died
89+
before the work".
90+
91+
== Rule 4 — Every soft-gate must be explained
92+
93+
`continue-on-error: true` (or any soft-gate) MUST be explained, in a
94+
comment within the 12 lines above it, in exactly one of two ways:
95+
96+
. *Suppressed gate* — a known-failing gate temporarily bolted open. The
97+
comment MUST contain `GATE DEACTIVATED <ISO-DATE>`, the *verified* root
98+
cause (not a guess — burble#39's guess was wrong), and the explicit
99+
single-line re-arm trigger. This form is debt and must carry a path
100+
back to armed.
101+
. *By-design advisory* — a step that is best-effort by nature (e.g.
102+
resilience to an upstream outage, optional enrichment). The comment
103+
MUST carry a `by-design:` or `advisory:` rationale. This form is not
104+
debt; it documents intent so reviewers do not mistake it for a
105+
suppressed gate.
106+
107+
A bare `continue-on-error` with neither is a policy violation, flagged by
108+
`tasks/tooling-integrity-lint.sh` (rule R4).
109+
110+
=== Rollout: advisory first, tighten later
111+
112+
R4 ships *advisory* (reported, non-blocking) by default; R1 (unversioned
113+
installs) is blocking immediately. This deliberately follows the same
114+
"advisory first, tighten later" gating doctrine the estate adopted for
115+
Hypatia SARIF (burble#35 item 3) — a new policy gate enforced as a
116+
hard estate-wide failure on day one would itself be bolted open, exactly
117+
the failure mode this policy exists to end. Promote R4 to blocking
118+
(`--strict`) per repo as its existing soft-gates are explained.
119+
120+
== Rule 5 — Resolve at source, never per-repo
121+
122+
Fixes to any of the above are made in the canonical source
123+
(`contractile` source under `contractiles/`, the canonical CI/Justfile
124+
templates in `rsr-template-repo` → `v3-templater` / `reposystem`) and
125+
propagated by re-adoption. Per-repo patches recreate the drift this
126+
policy exists to prevent.
127+
128+
NOTE: The `contractile` generator itself is currently out-of-band (the
129+
generator repository is not part of the working estate). Until it is
130+
in-tree, changes to generated `contractile.just` content are made in the
131+
`contractiles/` *source* fragments and regenerated by whoever holds the
132+
generator; consumers must re-run `contractile gen-just --dir contractiles`
133+
to pick them up. This gap is tracked estate-wide (see the Tooling Version
134+
Integrity sweep tracking issue).
135+
136+
== Enforcement
137+
138+
* `tasks/tooling-integrity-lint.sh` — scans `.github/workflows/` for
139+
unversioned family-tool installs (Rule 1) and bare `continue-on-error`
140+
(Rule 4). Exit non-zero on any violation.
141+
* `contractiles/must/Mustfile` (canonical template) and this repo's live
142+
`Mustfile` carry a `tooling-version-integrity` mandatory check that
143+
runs the lint — so every repo adopting the canonical `must` contract
144+
inherits the gate (Rule 3, Rule 5).

contractiles/must/Mustfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ checks:
3535
- name: no-template-residue
3636
description: "No placeholder text from the contractiles template must remain."
3737
run: "bash -uc '! rg -rn \"REPLACE-WITH|PLMP-1.0-or-later\" --type-not binary . | rg .'"
38+
39+
- name: tooling-version-integrity
40+
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."
41+
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; }'"

tasks/tooling-integrity-lint.sh

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
#
4+
# tooling-integrity-lint.sh — enforces the Hyperpolymath Tooling Version
5+
# Integrity Policy (see ../TOOLING-VERSION-INTEGRITY-POLICY.adoc).
6+
#
7+
# Scans .github/workflows/*.{yml,yaml} for:
8+
# R1 an unversioned rhyming-family tool install
9+
# (`tool: just` instead of `tool: just@<ver>`). ALWAYS BLOCKING:
10+
# few instances, unambiguously wrong, the burble#39 root cause.
11+
# R4 an *unexplained* `continue-on-error: true`. A soft-gate is
12+
# explained if, within 12 lines above it, there is EITHER
13+
# (a) `GATE DEACTIVATED <ISO-DATE>` — a dated, root-caused,
14+
# temporary suppression of a known-failing gate, OR
15+
# (b) `by-design:` / `advisory:` — a documented, intentional
16+
# best-effort step (e.g. upstream-outage resilience).
17+
# Bare, with neither, is a violation. ADVISORY by default
18+
# (reported, non-blocking) per the estate "advisory first,
19+
# tighten later" gating doctrine; pass --strict to make R4 block.
20+
#
21+
# Exit 0 = clean (or only advisory R4 findings in default mode).
22+
# Exit 1 = blocking violations.
23+
# Pure bash + grep/awk; no external deps. Run from a repo root.
24+
25+
set -uo pipefail
26+
27+
STRICT=0
28+
[ "${1:-}" = "--strict" ] && STRICT=1
29+
30+
FAMILY='just|must|trust|adjust|bust|dust|intend'
31+
WF_DIR=".github/workflows"
32+
r1=0
33+
r4=0
34+
35+
# --- R0: installed `just` must satisfy the import? floor (>= 1.19.0) --------
36+
# BLOCKING when `just` is present (this is the burble#39 invariant, and the
37+
# execution-proof check an in-file guard structurally cannot do). Skipped,
38+
# with a note, when `just` is absent (repo may not use it).
39+
if command -v just >/dev/null 2>&1; then
40+
jv=$(just --version 2>/dev/null | cut -d' ' -f2)
41+
maj=${jv%%.*}; rest=${jv#*.}; min=${rest%%.*}
42+
if [ -z "$jv" ] || ! { [ "${maj:-0}" -gt 1 ] 2>/dev/null || { [ "${maj:-0}" -eq 1 ] && [ "${min:-0}" -ge 19 ]; }; }; then
43+
echo "::error::[R0] just ${jv:-?} < 1.19.0 — import? unsupported (Tooling Version Integrity Rule 1/3)"
44+
echo "tooling-integrity-lint: FAIL — just below the import? floor"
45+
exit 1
46+
fi
47+
echo "tooling-integrity-lint: R0 OK — just $jv satisfies >= 1.19.0"
48+
else
49+
echo "tooling-integrity-lint: R0 skipped — just not on PATH"
50+
fi
51+
52+
[ -d "$WF_DIR" ] || { echo "tooling-integrity-lint: no $WF_DIR — nothing more to check"; exit 0; }
53+
54+
while IFS= read -r -d '' wf; do
55+
# --- R1: unversioned family-tool install (BLOCKING) ----------------------
56+
while IFS=: read -r lineno _; do
57+
[ -n "${lineno:-}" ] || continue
58+
echo "::error file=$wf,line=$lineno::[R1] unversioned family-tool install — pin 'tool: <name>@<version>' (Tooling Version Integrity Rule 1)"
59+
r1=$((r1 + 1))
60+
done < <(grep -nE "^[[:space:]]*tool:[[:space:]]*(${FAMILY})[[:space:]]*$" "$wf" 2>/dev/null)
61+
62+
# --- R4: unexplained continue-on-error (ADVISORY unless --strict) ---------
63+
while IFS=: read -r lineno _; do
64+
[ -n "${lineno:-}" ] || continue
65+
echo "::warning file=$wf,line=$lineno::[R4] unexplained continue-on-error — add a 'GATE DEACTIVATED <ISO-DATE>' suppression block OR a 'by-design:'/'advisory:' rationale (Tooling Version Integrity Rule 4)"
66+
r4=$((r4 + 1))
67+
done < <(awk '
68+
{ line[NR] = $0 }
69+
/^[[:space:]]*continue-on-error:[[:space:]]*true[[:space:]]*$/ {
70+
ok = 0
71+
for (i = NR-1; i >= NR-12 && i >= 1; i--) {
72+
if (line[i] ~ /GATE DEACTIVATED[[:space:]]+[0-9]{4}-[0-9]{2}-[0-9]{2}/) { ok = 1; break }
73+
if (line[i] ~ /(by-design|advisory):/) { ok = 1; break }
74+
}
75+
if (!ok) print NR ":"
76+
}' "$wf" 2>/dev/null)
77+
done < <(find "$WF_DIR" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 2>/dev/null)
78+
79+
echo "tooling-integrity-lint: R1(blocking)=$r1 R4(soft-gate)=$r4 strict=$STRICT"
80+
81+
if [ "$r1" -gt 0 ]; then
82+
echo "tooling-integrity-lint: FAIL — $r1 unversioned family-tool install(s). See TOOLING-VERSION-INTEGRITY-POLICY.adoc Rule 1"
83+
exit 1
84+
fi
85+
if [ "$STRICT" -eq 1 ] && [ "$r4" -gt 0 ]; then
86+
echo "tooling-integrity-lint: FAIL (--strict) — $r4 unexplained continue-on-error. See Rule 4"
87+
exit 1
88+
fi
89+
[ "$r4" -gt 0 ] && echo "tooling-integrity-lint: PASS with $r4 advisory R4 finding(s) (non-blocking; --strict to enforce)"
90+
[ "$r4" -eq 0 ] && echo "tooling-integrity-lint: OK — no violations"
91+
exit 0

0 commit comments

Comments
 (0)