Skip to content

Commit c598fbf

Browse files
fix(governance): package policy — cite the canonical ruling, detect sealed containers (#534)
Fixes `check-package-policy.sh`, which encoded the **superseded** packaging rule. ## Three defects 1. **Cited the wrong source.** It read *"POLICY (CLAUDE.md, Package Management)"*. That section of CLAUDE.md 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 at all.** 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, so a repo doing exactly the right thing was told it had no packaging. `Containerfile*` and `Dockerfile*` now satisfy it. 3. **Printed `✅ Nix package management detected (fallback)`.** Nix is not a tier. Blessing a flake as compliant is precisely 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 — it never prints a pass. ## ⚠ Draft — one thing to confirm: the retirement date Set to **2026-06-01** per your 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: | state | repos | |---|---| | **Nix-only → RED on merge** | **22** | | guix only | 160 | | both | 18 | | neither (already covered by the 2026-08-21 cutoff) | 33 | If `2027-06-01` was intended, it is one constant to change. Left as a draft rather than guessing. *(This also corrects my own earlier estimate — I had warned this would "red hundreds of repos". Measured, it is 22.)* ## Sequencing warning is built into the failure message 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**. For those, deleting the flake means having *no* working packaging. The error output says so explicitly and points at `spec/scaffold-stub-debt.adoc` step 3: make the Guix side real **first**, then retire the mirror. ## Red-teamed — all nine branches measured | case | exit | | |---|---|---| | `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 | 0 | guix wins, no nix warning | | malformed date | **1** | refuses to run | `bash -n` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 74f65c7 commit c598fbf

1 file changed

Lines changed: 80 additions & 10 deletions

File tree

scripts/check-package-policy.sh

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,31 @@
99
# an unconditional `✅ Package policy check passed` (standards#505). It could
1010
# not detect a violation, and it claimed a pass over any input.
1111
#
12-
# POLICY (CLAUDE.md, "Package Management"): Guix primary (guix.scm), Nix
13-
# fallback (flake.nix). A repo satisfying neither is the violation.
12+
# POLICY — canonical source is `rhodium-standard-repositories/spec/
13+
# LANGUAGE-POLICY.adoc` §Package Management, NOT CLAUDE.md:
14+
#
15+
# RULED 2026-05-18 (estate-wide): Guix primary + sealed-container escape;
16+
# NO Nix mirror. One packager per repo. A `flake.nix` that only mirrors a
17+
# Guix manifest is drift to remove, not a fallback. A second packager is
18+
# permitted only where it is the *sole* source of a *specific named*
19+
# dependency, and that dependency is documented as the reason.
20+
# **Supersedes the prior "Nix fallback everywhere" rule.**
21+
#
22+
# Tiers: Guix (guix.scm/manifest.scm) is PRIMARY; a sealed container
23+
# (Containerfile, Podman/Svalinn-sealed) is the ESCAPE HATCH for the
24+
# not-in-Guix / non-free tail. Nix is NOT a tier.
25+
#
26+
# This script previously cited CLAUDE.md and printed
27+
# `✅ Nix package management detected (fallback)`. CLAUDE.md's packaging
28+
# section is STALE — it still describes Nix as a fallback, in 472 copies
29+
# estate-wide — and CLAUDE.md itself defers to LANGUAGE-POLICY.adoc as
30+
# canonical, so the .adoc wins. Blessing a flake as compliant is what let the
31+
# 2026-07-21 remediation sweep ship `flake.nix` to 59 repos that should have
32+
# received Guix or a container.
33+
#
34+
# It also had NO sealed-container detection at all, so the policy's own escape
35+
# hatch could not satisfy the policy — a repo doing exactly the right thing for
36+
# the not-in-Guix tail was reported as having no packaging.
1437
#
1538
# PREDICATE — deliberately tightened. The previous step accepted *any* `*.scm`
1639
# anywhere in the tree as proof of "Guix package management detected", which a
@@ -37,6 +60,8 @@
3760
#
3861
# Environment (test seams — the shipped policy is the default in each case):
3962
# ENFORCE_PACKAGE_POLICY_FROM YYYY-MM-DD; enforcement begins ON this date.
63+
# ENFORCE_NIX_RETIREMENT_FROM YYYY-MM-DD; date Nix-only stops warning and
64+
# starts failing. Owner-set: 2026-06-01.
4065
# PKG_TODAY YYYY-MM-DD; overrides "now" so the pre-cutoff
4166
# and post-cutoff branches are both testable.
4267
#
@@ -91,32 +116,77 @@ find_first() {
91116

92117
GUIX="$(find_first -name guix.scm -o -name manifest.scm -o -name channels.scm -o -name .guix-channel)"
93118
NIX="$(find_first -name flake.nix -o -name default.nix -o -name shell.nix)"
119+
# Sealed container — the policy's named escape hatch, previously undetectable.
120+
# `Containerfile*` and `Dockerfile*` both count: the estate standardises on
121+
# Podman/Containerfile, but a repo already carrying a Dockerfile is served by
122+
# the same escape hatch and should not be told it has no packaging.
123+
CONTAINER="$(find_first -name 'Containerfile*' -o -name 'Dockerfile*')"
94124

95125
if [ -n "$GUIX" ]; then
96126
echo "✅ Guix package management detected (primary): ${GUIX#"$ROOT"/}"
97127
exit 0
98128
fi
99129

100-
if [ -n "$NIX" ]; then
101-
echo "✅ Nix package management detected (fallback): ${NIX#"$ROOT"/}"
102-
echo "::notice::Guix is the estate primary; Nix is the accepted fallback."
130+
if [ -n "$CONTAINER" ]; then
131+
echo "✅ Sealed-container packaging detected (escape hatch): ${CONTAINER#"$ROOT"/}"
132+
echo "::notice::Guix is the estate primary; a sealed container is the" \
133+
"accepted escape hatch for the not-in-Guix / non-free tail."
103134
exit 0
104135
fi
105136

137+
# Nix-only. Under the 2026-05-18 ruling this is NOT compliance — Nix is not a
138+
# tier — but it is also not the same as having no packaging at all, and the
139+
# repos in this state are overwhelmingly there because a *sweep put them there*
140+
# rather than through any author's choice. So it warns until the retirement
141+
# date, then fails. It never prints a ✅.
142+
#
143+
# ⚠ SEQUENCING — read before changing ENFORCE_NIX_RETIREMENT_FROM.
144+
# Nix retirement must TRAIL per-repo Guix functionality. Campaign #102 closed
145+
# COMPLETED having hand-diffed 277 candidates and removed exactly ONE flake;
146+
# ~270 repos carry a `guix.scm` that is a non-functional scaffold stub, so for
147+
# them "delete the flake" means "have no working packaging". Measured over the
148+
# local estate checkout: 22 repos are Nix-only and would fail the moment this
149+
# date passes. Setting a date in the past makes that immediate, with no grace.
150+
if [ -n "$NIX" ]; then
151+
ENFORCE_NIX_RETIREMENT_FROM="${ENFORCE_NIX_RETIREMENT_FROM:-2026-06-01}"
152+
require_date ENFORCE_NIX_RETIREMENT_FROM "$ENFORCE_NIX_RETIREMENT_FROM"
153+
154+
if [[ "$TODAY" < "$ENFORCE_NIX_RETIREMENT_FROM" ]]; then
155+
echo "::warning::Nix-only packaging (${NIX#"$ROOT"/}). Nix is NOT an estate" \
156+
"tier — Guix is primary, sealed container is the escape hatch. This" \
157+
"becomes a BLOCKING failure on $ENFORCE_NIX_RETIREMENT_FROM (today is $TODAY)."
158+
echo "NOT YET ENFORCED: Nix-only packaging inside the retirement grace window."
159+
exit 0
160+
fi
161+
162+
echo "::error::Nix-only packaging is not compliant: ${NIX#"$ROOT"/}"
163+
echo
164+
echo "Estate policy (LANGUAGE-POLICY.adoc, RULED 2026-05-18) is Guix primary"
165+
echo "+ sealed-container escape; NO Nix mirror. Replace the flake with:"
166+
echo " guix.scm | manifest.scm | channels.scm | .guix-channel (primary)"
167+
echo " Containerfile (escape hatch)"
168+
echo
169+
echo "Do NOT simply delete the flake: a repo whose guix.scm is a scaffold stub"
170+
echo "has no working packaging once the flake is gone. Make the Guix side real"
171+
echo "first, then retire the mirror (spec/scaffold-stub-debt.adoc, step 3)."
172+
exit 1
173+
fi
174+
106175
# Violation: neither packaging system is present.
107176
if [[ "$TODAY" < "$ENFORCE_PACKAGE_POLICY_FROM" ]]; then
108-
echo "::warning::No Guix or Nix packaging found — this becomes a BLOCKING" \
109-
"failure on $ENFORCE_PACKAGE_POLICY_FROM (today is $TODAY)."
177+
echo "::warning::No packaging found (no Guix, no sealed container) — this" \
178+
"becomes a BLOCKING failure on $ENFORCE_PACKAGE_POLICY_FROM (today is $TODAY)."
110179
# Never claim a pass while the policy is unmet.
111180
echo "NOT YET ENFORCED: package policy unmet but inside the grace window."
112181
exit 0
113182
fi
114183

115-
echo "::error::Package policy violation: no Guix or Nix packaging found."
184+
echo "::error::Package policy violation: no packaging found."
116185
echo
117-
echo "Estate policy is Guix primary / Nix fallback. Add one of:"
186+
echo "Estate policy (LANGUAGE-POLICY.adoc, RULED 2026-05-18) is Guix primary"
187+
echo "+ sealed-container escape; NO Nix mirror. Add one of:"
118188
echo " guix.scm | manifest.scm | channels.scm | .guix-channel (primary)"
119-
echo " flake.nix | default.nix | shell.nix (fallback)"
189+
echo " Containerfile (escape hatch)"
120190
echo
121191
echo "Files inside .git/ node_modules/ deps/ .lake/ vendor/ do not count."
122192
exit 1

0 commit comments

Comments
 (0)