Skip to content

Commit a24c785

Browse files
chore!: eradicate all Python — total ban, no exceptions (#279)
## Why The org Python ban is now **total, with no exceptions** (no SaltStack-style carve-outs). This removes every Python site in hypatia and replaces each with a faithful, verified **Rust** port. Also unblocks the `governance / Language / package anti-pattern policy → Check for Python (non-SaltStack)` gate that was failing **unrelated** PRs (e.g. #270, a pure `just` version pin) as inherited baseline rot — not per-PR defects. ## What | Was (Python) | Now (Rust) | |---|---| | `scripts/check-bench-regression.py` | `scripts/bench-tools` → `check-bench-regression` | | `scripts/update-bench-baselines.py` | `scripts/bench-tools` → `update-bench-baselines` | | inline `python3`+`jsonschema` in `build-gossamer-gui.yml` | `scripts/ci-tools` → `validate-panll-harness` (TOML → JSON-Schema Draft 2020-12) | | inline `python3` in `ci.yml` | `scripts/ci-tools` → `check-k9iser-paths` | - `bench-tools` is zero-dependency (was authored on the #272 lineage; **vendored here so this PR is self-contained** and order-independent). - `build-gossamer-gui.yml` loses its `pip install jsonschema` step entirely. - Callers rewired: `tests.yml` (×2), `build-gossamer-gui.yml`, `ci.yml`, benchmarks README. - Exemptions **revoked** (no exceptions): `.hypatia-ignore` block, `.hypatia-exemptions.md` rows, `.hypatia-baseline.json` entries. ## Verification - All 4 Rust bins build clean (cargo 1.95.0). - `check-k9iser-paths`: real `k9iser.toml` → `OK … 5 source(s), 3 constraint(s)`, exit 0. - `validate-panll-harness`: real `panll.harness.toml` vs the **live** panll-harness/v2 schema → `OK …`, exit 0. - Tracked `.py` files: **2 → 0**. ## Follow-up (separate, standards-level) The governance rule itself should stop honouring `banned_language_file` exemptions now the ban is total — likely an estate-wide standards change, out of scope here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 65f0912 commit a24c785

18 files changed

Lines changed: 2677 additions & 348 deletions

.github/workflows/build-gossamer-gui.yml

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,11 @@ jobs:
7373
exit 1
7474
fi
7575
76-
- name: Install jsonschema
76+
- name: Build ci-tools (panll-harness validator)
7777
run: |
7878
set -euo pipefail
79-
python3 -m pip install --user --quiet jsonschema
79+
cargo build --release --manifest-path scripts/ci-tools/Cargo.toml \
80+
--bin validate-panll-harness
8081
8182
- name: Fetch panll-harness v2 schema
8283
run: |
@@ -90,23 +91,9 @@ jobs:
9091
- name: Validate panll.harness.toml against panll-harness/v2
9192
run: |
9293
set -euo pipefail
93-
python3 - <<'PY'
94-
import json, sys, tomllib
95-
from jsonschema import Draft202012Validator, validate
96-
97-
with open("src/ui/gossamer/panll.harness.toml", "rb") as fh:
98-
data = tomllib.load(fh)
99-
with open("/tmp/schemas/panll-harness-v2.schema.json") as fh:
100-
schema = json.load(fh)
101-
102-
Draft202012Validator.check_schema(schema)
103-
try:
104-
validate(instance=data, schema=schema)
105-
except Exception as exc:
106-
print(f"::error::panll.harness.toml fails panll-harness/v2 validation: {exc}")
107-
sys.exit(1)
108-
print("OK panll.harness.toml validates against panll-harness/v2")
109-
PY
94+
./scripts/ci-tools/target/release/validate-panll-harness \
95+
src/ui/gossamer/panll.harness.toml \
96+
/tmp/schemas/panll-harness-v2.schema.json
11097
11198
loader-smoke:
11299
name: Gossamer loader smoke harness
@@ -152,7 +139,13 @@ jobs:
152139
- name: Install just
153140
uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1
154141
with:
155-
tool: just
142+
# Pin an explicit modern just: the Justfile uses `import?`
143+
# (optional import, just >= 1.19.0). Unversioned `tool: just`
144+
# resolves via this action's bundled manifest, which can ship
145+
# an old just (e.g. 1.14.0) -> `error: Unknown start of token`
146+
# at `import? "contractile.just"`, silently breaking the build.
147+
# Estate Tooling Version Integrity policy (root cause: burble#39).
148+
tool: just@1.34.0
156149

157150
- name: Cache Ephapax build
158151
id: cache-ephapax

.github/workflows/ci.yml

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -225,27 +225,9 @@ jobs:
225225
- name: Parse k9iser.toml + verify declared paths exist
226226
run: |
227227
set -euo pipefail
228-
python3 - <<'PY'
229-
import tomllib, sys, pathlib
230-
231-
with open("k9iser.toml", "rb") as fh:
232-
data = tomllib.load(fh)
233-
234-
missing = []
235-
for src in data.get("source", []):
236-
p = pathlib.Path(src["path"])
237-
if not p.exists():
238-
missing.append(src["path"])
239-
240-
if missing:
241-
for m in missing:
242-
print(f"::error::k9iser.toml declares missing source {m}")
243-
sys.exit(1)
244-
245-
n = len(data.get("source", []))
246-
c = len(data.get("constraint", []))
247-
print(f"OK k9iser.toml parses — {n} source(s), {c} constraint(s)")
248-
PY
228+
cargo build --release --manifest-path scripts/ci-tools/Cargo.toml \
229+
--bin check-k9iser-paths
230+
./scripts/ci-tools/target/release/check-k9iser-paths k9iser.toml
249231
250232
- name: Run k9iser build (if CLI available)
251233
run: |

.github/workflows/tests.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,8 @@ jobs:
647647
if: ${{ github.event.inputs.mode != 'regenerate-baseline' }}
648648
run: |
649649
set -euo pipefail
650-
python3 scripts/check-bench-regression.py \
650+
cargo build --release --manifest-path scripts/bench-tools/Cargo.toml
651+
./scripts/bench-tools/target/release/check-bench-regression \
651652
bench-output.txt \
652653
.machine_readable/benchmarks/baselines.json \
653654
| tee -a "$GITHUB_STEP_SUMMARY"
@@ -656,7 +657,8 @@ jobs:
656657
if: ${{ github.event.inputs.mode == 'regenerate-baseline' }}
657658
run: |
658659
set -euo pipefail
659-
python3 scripts/update-bench-baselines.py \
660+
cargo build --release --manifest-path scripts/bench-tools/Cargo.toml
661+
./scripts/bench-tools/target/release/update-bench-baselines \
660662
bench-output.txt \
661663
.machine_readable/benchmarks/baselines.json
662664
echo "## Regenerated baseline" >> "$GITHUB_STEP_SUMMARY"

.hypatia-baseline.json

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,4 @@
11
[
2-
{
3-
"severity": "critical",
4-
"rule_module": "cicd_rules",
5-
"type": "banned_language_file",
6-
"file": "scripts/check-bench-regression.py",
7-
"action": "flag"
8-
},
9-
{
10-
"severity": "critical",
11-
"rule_module": "cicd_rules",
12-
"type": "banned_language_file",
13-
"file": "scripts/update-bench-baselines.py",
14-
"action": "flag"
15-
},
162
{
173
"severity": "critical",
184
"rule_module": "code_safety",

.hypatia-exemptions.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ already placed at each file's site.
1919

2020
| File | Rule | Inline marker | Rationale | Revisit when |
2121
|---|---|---|---|---|
22-
| `scripts/update-bench-baselines.py` | `cicd_rules/banned_language_file` | `# hypatia:ignore cicd_rules/banned_language_file` (line 3) | Parses criterion's bencher-format output; criterion's tooling assumes Python downstream. | A maintained Rust/shell parser exists for criterion bencher format. |
23-
| `scripts/check-bench-regression.py` | `cicd_rules/banned_language_file` | `# hypatia:ignore cicd_rules/banned_language_file` (line 3) | Pair of the above. | Same. |
2422
| `src/abi/RuleEngine.idr` | `code_safety/believe_me`, `structural_drift/SD008` | `-- hypatia:ignore code_safety/believe_me structural_drift/SD008` (line 19) | The scanner is counting the literal token `believe_me` inside an Idris2 comment that asserts there are *no* such primitives. There is no actual `believe_me` call site in the module. | The scanner learns to skip comment lines (token vs syntactic match). |
2523

2624
## Audit-training and remediation-script corpora

.hypatia-ignore

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,8 @@
2323
#
2424
# This file is for exemptions that span a whole file or directory.
2525

26-
# ─── Python bench helpers ───────────────────────────────────────────────
27-
#
28-
# Scoped exemption — RSR org policy bans Python except SaltStack. These two
29-
# scripts are bench-data helpers used only by .github/workflows/bench.yml;
30-
# they parse criterion output and update baseline JSON. Rust/Julia port is
31-
# tracked but not blocking. Until the port lands, suppress the
32-
# banned_language_file finding on these two specific paths so the gate
33-
# treats them as a known, documented carve-out rather than baseline noise.
34-
cicd_rules/banned_language_file:scripts/check-bench-regression.py
35-
cicd_rules/banned_language_file:scripts/update-bench-baselines.py
26+
# ─── Python bench helpers — REVOKED ─────────────────────────────────────
27+
#
28+
# The two scripts/*.py bench helpers were removed and re-implemented in
29+
# Rust (scripts/bench-tools/). The org Python ban is now total with no
30+
# exceptions, so this carve-out is deleted rather than carried.

.machine_readable/benchmarks/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ cargo bench --bench hypatia_bench -- \
8484
| tee /tmp/bench.txt
8585

8686
# Parse the output and update baselines.json:
87-
python3 scripts/update-bench-baselines.py /tmp/bench.txt \
87+
cargo build --release --manifest-path scripts/bench-tools/Cargo.toml
88+
./scripts/bench-tools/target/release/update-bench-baselines /tmp/bench.txt \
8889
.machine_readable/benchmarks/baselines.json
8990
```
9091

scripts/bench-tools/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/bench-tools/Cargo.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
#
3+
# Standalone bench-data tooling — deliberately NOT a workspace member so it
4+
# never perturbs the main build / proof gates, and zero-dependency so CI
5+
# needs no crates.io fetch. Replaces the former scripts/*.py (org policy
6+
# bans Python outside SaltStack; see standards Explicit-Escape Principle).
7+
# Empty table: keep this crate out of the repo's main Cargo workspace so it
8+
# never perturbs the main build / proof gates.
9+
[workspace]
10+
11+
[package]
12+
name = "bench-tools"
13+
version = "0.1.0"
14+
edition = "2021"
15+
license = "PMPL-1.0-or-later"
16+
publish = false
17+
18+
[lib]
19+
path = "src/lib.rs"
20+
21+
[[bin]]
22+
name = "check-bench-regression"
23+
path = "src/bin/check-bench-regression.rs"
24+
25+
[[bin]]
26+
name = "update-bench-baselines"
27+
path = "src/bin/update-bench-baselines.rs"
28+
29+
[profile.release]
30+
opt-level = 1
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
//
3+
// check-bench-regression — compare a criterion bencher run against
4+
// .machine_readable/benchmarks/baselines.json and fail if any benchmark
5+
// regressed by more than the configured threshold. A faithful Rust port of
6+
// the former scripts/check-bench-regression.py (org policy bans Python
7+
// outside SaltStack). Pairs with update-bench-baselines.
8+
//
9+
// Usage:
10+
// check-bench-regression <bencher-output> <baselines.json>
11+
//
12+
// Exit status: 0 = no regressions over threshold (or no baselines yet),
13+
// 1 = at least one regression, 2 = usage / file error.
14+
//
15+
// Markdown summary -> stdout (for $GITHUB_STEP_SUMMARY); `::error::`
16+
// annotations -> stderr.
17+
18+
use bench_tools::{fmt_ns, parse_bencher_output, parse_json, Json};
19+
use std::process::exit;
20+
21+
fn main() {
22+
let argv: Vec<String> = std::env::args().collect();
23+
if argv.len() != 3 {
24+
eprintln!("usage: check-bench-regression <bencher-output> <baselines.json>");
25+
exit(2);
26+
}
27+
let current_path = &argv[1];
28+
let baselines_path = &argv[2];
29+
30+
let current_text = match std::fs::read_to_string(current_path) {
31+
Ok(t) => t,
32+
Err(_) => {
33+
eprintln!("error: {current_path} missing");
34+
exit(2);
35+
}
36+
};
37+
38+
let mut current = parse_bencher_output(&current_text);
39+
current.sort_by(|a, b| a.0.cmp(&b.0)); // Python iterates `sorted(current.items())`
40+
41+
if current.is_empty() {
42+
println!(
43+
"::warning::no bench lines parsed from current run \u{2014} \
44+
did criterion use --output-format bencher?"
45+
);
46+
exit(0);
47+
}
48+
49+
let baseline_doc: Json = match std::fs::read_to_string(baselines_path) {
50+
Ok(t) => match parse_json(&t) {
51+
Ok(v) => v,
52+
Err(_) => {
53+
println!(
54+
"::warning::{baselines_path} is not valid JSON; \
55+
treating as empty baseline"
56+
);
57+
Json::Obj(vec![])
58+
}
59+
},
60+
Err(_) => Json::Obj(vec![]),
61+
};
62+
63+
let baselines: Vec<(String, f64)> = match baseline_doc.get("baselines") {
64+
Some(Json::Obj(p)) => p
65+
.iter()
66+
.filter_map(|(k, v)| v.as_f64().map(|n| (k.clone(), n)))
67+
.collect(),
68+
_ => vec![],
69+
};
70+
let lookup = |name: &str| baselines.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
71+
72+
let threshold_pct = baseline_doc
73+
.get("_regression_threshold_pct")
74+
.and_then(|v| v.as_f64())
75+
.unwrap_or(50.0);
76+
77+
if baselines.is_empty() {
78+
println!("## Benchmark run (advisory mode \u{2014} no baselines yet)");
79+
println!();
80+
println!("| Benchmark | Current |");
81+
println!("|-----------|---------|");
82+
for (name, ns) in &current {
83+
println!("| `{name}` | {} |", fmt_ns(*ns));
84+
}
85+
println!();
86+
println!(
87+
"_No entries in `baselines.json` yet \u{2014} see \
88+
`.machine_readable/benchmarks/README.md` for how to seed them._"
89+
);
90+
exit(0);
91+
}
92+
93+
let mut regressions: Vec<(String, i64, i64, f64)> = vec![];
94+
let mut rows: Vec<(String, String, String, String, String)> = vec![];
95+
96+
for (name, ns_now) in &current {
97+
let ns_now = *ns_now;
98+
match lookup(name) {
99+
None => rows.push((
100+
name.clone(),
101+
fmt_ns(ns_now),
102+
"\u{2014}".into(),
103+
"new".into(),
104+
"\u{2728}".into(),
105+
)),
106+
Some(ns_base) => {
107+
let pct = if ns_base != 0.0 {
108+
(ns_now as f64 - ns_base) / ns_base * 100.0
109+
} else {
110+
0.0
111+
};
112+
let mut verdict = "\u{2705}";
113+
if pct > threshold_pct {
114+
verdict = "\u{274c}";
115+
regressions.push((name.clone(), ns_base as i64, ns_now, pct));
116+
} else if pct > threshold_pct / 2.0 {
117+
verdict = "\u{26a0}\u{fe0f}";
118+
} else if pct < -10.0 {
119+
verdict = "\u{1f680}";
120+
}
121+
rows.push((
122+
name.clone(),
123+
fmt_ns(ns_now),
124+
fmt_ns(ns_base as i64),
125+
format!("{pct:+.1}%"),
126+
verdict.into(),
127+
));
128+
}
129+
}
130+
}
131+
132+
println!("## Benchmark comparison");
133+
println!();
134+
println!("Threshold: regression > **{threshold_pct:.0}%** fails CI.");
135+
println!();
136+
println!("| Benchmark | Current | Baseline | \u{0394} | |");
137+
println!("|-----------|---------|----------|---|---|");
138+
for (a, b, c, d, e) in &rows {
139+
println!("| `{a}` | {b} | {c} | {d} | {e} |");
140+
}
141+
println!();
142+
143+
if !regressions.is_empty() {
144+
println!("### Regressions exceeding threshold");
145+
println!();
146+
for (name, ns_base, ns_now, pct) in &regressions {
147+
let msg = format!(
148+
"{name}: {} \u{2192} {} ({pct:+.1}%, threshold {threshold_pct:.0}%)",
149+
fmt_ns(*ns_base),
150+
fmt_ns(*ns_now),
151+
);
152+
println!("- {msg}");
153+
eprintln!("::error::benchmark regression: {msg}");
154+
}
155+
exit(1);
156+
}
157+
}

0 commit comments

Comments
 (0)