|
| 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). |
0 commit comments