From 4c3b0d548f91d58bb15ad71db918bdfcf50e13e6 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:32:09 +0100 Subject: [PATCH] =?UTF-8?q?fix(governance):=20package=20policy=20=E2=80=94?= =?UTF-8?q?=20cite=20the=20canonical=20ruling,=20detect=20containers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three defects in check-package-policy.sh, all stemming from it encoding a superseded rule. 1. Cited the WRONG SOURCE. It read "POLICY (CLAUDE.md, 'Package Management')". CLAUDE.md's packaging section is stale — it still calls Nix a fallback, in 472 copies estate-wide — and CLAUDE.md itself defers to spec/LANGUAGE-POLICY.adoc as canonical. Now quotes the actual ruling: RULED 2026-05-18 (estate-wide): Guix primary + sealed-container escape; NO Nix mirror. ... Supersedes the prior "Nix fallback everywhere" rule. 2. NO sealed-container detection. The policy names a sealed container (Podman/Containerfile, Svalinn-sealed) as the escape hatch for the not-in-Guix / non-free tail, but the script only looked for Guix and Nix. A repo doing exactly the right thing was reported as having no packaging. Containerfile* and Dockerfile* now satisfy the policy. 3. Printed `✅ Nix package management detected (fallback)`. Nix is not a tier. Blessing a flake as compliant is what let the 2026-07-21 sweep ship flake.nix to 59 repos that should have received Guix or a container. Nix-only now warns, then fails on ENFORCE_NIX_RETIREMENT_FROM. It never prints a pass. Retirement date set to 2026-06-01 per owner decision. ⚠ That date is in the PAST (today is 2026-07-27), so Nix-only fails IMMEDIATELY with no grace window — unlike every other gate in this suite. Measured over the local estate checkout, 22 repos are Nix-only and go red on merge. Raised as a draft for confirmation in case 2027-06-01 was intended; it is one constant to change either way. The failure message explicitly warns against simply deleting the flake: campaign #102 closed COMPLETED having hand-diffed 277 candidates and removed exactly ONE, and ~270 repos carry a guix.scm that is a non-functional scaffold stub — for them, deleting the flake means having no working packaging. Make the Guix side real first (spec/scaffold-stub-debt.adoc, step 3). Red-teamed — all nine branches measured, not asserted: guix.scm -> 0 ✅ primary Containerfile -> 0 ✅ escape hatch Dockerfile -> 0 ✅ escape hatch nix-only, pre-retirement -> 0 warning + NOT YET ENFORCED nix-only, post-retirement -> 1 ::error:: no packaging, pre-cutoff -> 0 warning + NOT YET ENFORCED no packaging, post-cutoff -> 1 ::error:: guix + nix both present -> 0 guix wins, no nix warning malformed date -> 1 refuses to run `bash -n` clean. Co-Authored-By: Claude Opus 5 --- scripts/check-package-policy.sh | 90 +++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 10 deletions(-) diff --git a/scripts/check-package-policy.sh b/scripts/check-package-policy.sh index d79a34ce..a42ee7f9 100755 --- a/scripts/check-package-policy.sh +++ b/scripts/check-package-policy.sh @@ -9,8 +9,31 @@ # an unconditional `✅ Package policy check passed` (standards#505). It could # not detect a violation, and it claimed a pass over any input. # -# POLICY (CLAUDE.md, "Package Management"): Guix primary (guix.scm), Nix -# fallback (flake.nix). A repo satisfying neither is the violation. +# POLICY — canonical source is `rhodium-standard-repositories/spec/ +# LANGUAGE-POLICY.adoc` §Package Management, NOT CLAUDE.md: +# +# RULED 2026-05-18 (estate-wide): Guix primary + sealed-container escape; +# NO Nix mirror. One packager per repo. A `flake.nix` that only mirrors a +# Guix manifest is drift to remove, not a fallback. A second packager is +# permitted only where it is the *sole* source of a *specific named* +# dependency, and that dependency is documented as the reason. +# **Supersedes the prior "Nix fallback everywhere" rule.** +# +# Tiers: Guix (guix.scm/manifest.scm) is PRIMARY; a sealed container +# (Containerfile, Podman/Svalinn-sealed) is the ESCAPE HATCH for the +# not-in-Guix / non-free tail. Nix is NOT a tier. +# +# This script previously cited CLAUDE.md and printed +# `✅ Nix package management detected (fallback)`. CLAUDE.md's packaging +# section is STALE — it still describes Nix as a fallback, in 472 copies +# estate-wide — and CLAUDE.md itself defers to LANGUAGE-POLICY.adoc as +# canonical, so the .adoc wins. Blessing a flake as compliant is what let the +# 2026-07-21 remediation sweep ship `flake.nix` to 59 repos that should have +# received Guix or a container. +# +# It also had NO sealed-container detection at all, so the policy's own escape +# hatch could not satisfy the policy — a repo doing exactly the right thing for +# the not-in-Guix tail was reported as having no packaging. # # PREDICATE — deliberately tightened. The previous step accepted *any* `*.scm` # anywhere in the tree as proof of "Guix package management detected", which a @@ -37,6 +60,8 @@ # # Environment (test seams — the shipped policy is the default in each case): # ENFORCE_PACKAGE_POLICY_FROM YYYY-MM-DD; enforcement begins ON this date. +# ENFORCE_NIX_RETIREMENT_FROM YYYY-MM-DD; date Nix-only stops warning and +# starts failing. Owner-set: 2026-06-01. # PKG_TODAY YYYY-MM-DD; overrides "now" so the pre-cutoff # and post-cutoff branches are both testable. # @@ -91,32 +116,77 @@ find_first() { GUIX="$(find_first -name guix.scm -o -name manifest.scm -o -name channels.scm -o -name .guix-channel)" NIX="$(find_first -name flake.nix -o -name default.nix -o -name shell.nix)" +# Sealed container — the policy's named escape hatch, previously undetectable. +# `Containerfile*` and `Dockerfile*` both count: the estate standardises on +# Podman/Containerfile, but a repo already carrying a Dockerfile is served by +# the same escape hatch and should not be told it has no packaging. +CONTAINER="$(find_first -name 'Containerfile*' -o -name 'Dockerfile*')" if [ -n "$GUIX" ]; then echo "✅ Guix package management detected (primary): ${GUIX#"$ROOT"/}" exit 0 fi -if [ -n "$NIX" ]; then - echo "✅ Nix package management detected (fallback): ${NIX#"$ROOT"/}" - echo "::notice::Guix is the estate primary; Nix is the accepted fallback." +if [ -n "$CONTAINER" ]; then + echo "✅ Sealed-container packaging detected (escape hatch): ${CONTAINER#"$ROOT"/}" + echo "::notice::Guix is the estate primary; a sealed container is the" \ + "accepted escape hatch for the not-in-Guix / non-free tail." exit 0 fi +# Nix-only. Under the 2026-05-18 ruling this is NOT compliance — Nix is not a +# tier — but it is also not the same as having no packaging at all, and the +# repos in this state are overwhelmingly there because a *sweep put them there* +# rather than through any author's choice. So it warns until the retirement +# date, then fails. It never prints a ✅. +# +# ⚠ SEQUENCING — read before changing ENFORCE_NIX_RETIREMENT_FROM. +# Nix retirement must TRAIL per-repo Guix functionality. Campaign #102 closed +# COMPLETED having hand-diffed 277 candidates and removed exactly ONE flake; +# ~270 repos carry a `guix.scm` that is a non-functional scaffold stub, so for +# them "delete the flake" means "have no working packaging". Measured over the +# local estate checkout: 22 repos are Nix-only and would fail the moment this +# date passes. Setting a date in the past makes that immediate, with no grace. +if [ -n "$NIX" ]; then + ENFORCE_NIX_RETIREMENT_FROM="${ENFORCE_NIX_RETIREMENT_FROM:-2026-06-01}" + require_date ENFORCE_NIX_RETIREMENT_FROM "$ENFORCE_NIX_RETIREMENT_FROM" + + if [[ "$TODAY" < "$ENFORCE_NIX_RETIREMENT_FROM" ]]; then + echo "::warning::Nix-only packaging (${NIX#"$ROOT"/}). Nix is NOT an estate" \ + "tier — Guix is primary, sealed container is the escape hatch. This" \ + "becomes a BLOCKING failure on $ENFORCE_NIX_RETIREMENT_FROM (today is $TODAY)." + echo "NOT YET ENFORCED: Nix-only packaging inside the retirement grace window." + exit 0 + fi + + echo "::error::Nix-only packaging is not compliant: ${NIX#"$ROOT"/}" + echo + echo "Estate policy (LANGUAGE-POLICY.adoc, RULED 2026-05-18) is Guix primary" + echo "+ sealed-container escape; NO Nix mirror. Replace the flake with:" + echo " guix.scm | manifest.scm | channels.scm | .guix-channel (primary)" + echo " Containerfile (escape hatch)" + echo + echo "Do NOT simply delete the flake: a repo whose guix.scm is a scaffold stub" + echo "has no working packaging once the flake is gone. Make the Guix side real" + echo "first, then retire the mirror (spec/scaffold-stub-debt.adoc, step 3)." + exit 1 +fi + # Violation: neither packaging system is present. if [[ "$TODAY" < "$ENFORCE_PACKAGE_POLICY_FROM" ]]; then - echo "::warning::No Guix or Nix packaging found — this becomes a BLOCKING" \ - "failure on $ENFORCE_PACKAGE_POLICY_FROM (today is $TODAY)." + echo "::warning::No packaging found (no Guix, no sealed container) — this" \ + "becomes a BLOCKING failure on $ENFORCE_PACKAGE_POLICY_FROM (today is $TODAY)." # Never claim a pass while the policy is unmet. echo "NOT YET ENFORCED: package policy unmet but inside the grace window." exit 0 fi -echo "::error::Package policy violation: no Guix or Nix packaging found." +echo "::error::Package policy violation: no packaging found." echo -echo "Estate policy is Guix primary / Nix fallback. Add one of:" +echo "Estate policy (LANGUAGE-POLICY.adoc, RULED 2026-05-18) is Guix primary" +echo "+ sealed-container escape; NO Nix mirror. Add one of:" echo " guix.scm | manifest.scm | channels.scm | .guix-channel (primary)" -echo " flake.nix | default.nix | shell.nix (fallback)" +echo " Containerfile (escape hatch)" echo echo "Files inside .git/ node_modules/ deps/ .lake/ vendor/ do not count." exit 1