Skip to content

Commit d7f1b49

Browse files
fix(ci): revive Rust CI — it had never run (60/60 startup failures) (#358)
## The finding `.github/workflows/rust-ci.yml` **failed 60 consecutive runs, every one at 0s**, with zero jobs and no logs, from 2026-06-27 to 2026-07-27. That is a *startup failure* — GitHub rejected the file before scheduling anything. Not a flake, not a failing test. It was found via a **workflow-name fallback**: GitHub reports a workflow's `name` as its *file path* when it has never successfully parsed the file. ``` {"name": ".github/workflows/rust-ci.yml", "path": ".github/workflows/rust-ci.yml"} <- this one {"name": "Governance", "path": ".github/workflows/governance.yml"} {"name": "Coq Build (formal/)", "path": ".github/workflows/coq-build.yml"} ``` `rust-ci.yml` declares `name: Rust CI`. GitHub never saw it. ## Why it matters `cargo build` and `cargo test` exist **only** in this workflow. So the Rust implementation — 70 source files — had **no build or test gate for a month**, while the board read 13/14 green. The `wasm-validate` job guarding against invalid emitted wasm (cf. #348) was equally dead. ## Cause `if: hashFiles('Cargo.toml') != ''` used as a **job-level** conditional on both local jobs. Job-level `if:` is evaluated server-side before any checkout, so `hashFiles()` has no workspace to hash. The estate reusable's own header names this exact construct as a recurring failure mode, and solves it correctly with a `detect` job whose output gates the rest. The guard was redundant anyway — this repo unconditionally has a `Cargo.toml`. ## Ruled out first So the next reader doesn't repeat the work: | Hypothesis | Result | |---|---| | Encoding trap (CRLF / BOM / tabs) | **Falsified** — clean; only non-ASCII is an em-dash in a comment | | Invalid YAML | **Falsified** — parses | | Unresolvable action SHA pins | **Falsified** — all three resolve | | Reusable requests perms caller doesn't grant | **Falsified** — both exactly `contents: read` | ## Also in this PR — salvage from uncommitted sweep debris **Kept (useful):** - `.editorconfig` — per-language indent widths (rust/zig 4, ada 3, Justfile 4), but with the SPDX header **restored** and the sweep's rebrand of this file to `RSR-template-repo` (a template self-name leak) dropped. - `.gitignore` — build/tooling artefact patterns, **minus** the sweep's `.claude/` and `.editorconfig` entries: both are tracked here, so ignoring them would hide real changes from `git status`. Verified no tracked file becomes newly ignored. **Discarded (harmful):** - `guix.scm` clobbered to identify as `squisher-corpus`, **relicensed to PMPL-1.0-or-later**, Owner line deleted — and carrying a `//` comment in a Scheme file. - Deletion of `.tool-versions` (pins `coq 8.18.0` / `ocaml 5.4.1` / `rust stable`). - `.gitattributes` regression dropping `*.zig` and `*.a2ml` rules. - Commit `55dbd3b`, which replaced a valid CodeQL pin with `29b1f65c` — **a SHA that does not exist (HTTP 422)** — and would have taken the currently-green CodeQL gate down. ## How to verify The check is self-demonstrating: **if this PR's Rust CI runs at all, the fix worked.** Sixty prior runs produced zero jobs. Confirm the workflow now reports as `Rust CI` rather than its path, and that `no-default-features` and `wasm-validate` actually execute. ## Note for follow-up (not in this PR) The same detector found two more `name == path` entries here. Both are benign, but worth recording: - `workflow-linter.yml` — **file deleted**, registration lingers. Harmless orphan. - `instant-sync.yml` — file exists, `disabled_manually` (deliberately frozen). It parses as YAML, so its cause differs; it would need diagnosis before being re-enabled. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents bcaa977 + c1c81bf commit d7f1b49

27 files changed

Lines changed: 776 additions & 411 deletions

File tree

.editorconfig

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# SPDX-License-Identifier: MPL-2.0
22
# .editorconfig — RSR standard editor configuration
3+
# https://editorconfig.org
34

45
root = true
56

@@ -11,3 +12,44 @@ end_of_line = lf
1112
trim_trailing_whitespace = true
1213
insert_final_newline = true
1314

15+
# Prose formats rely on trailing double-space for hard line breaks.
16+
[*.md]
17+
trim_trailing_whitespace = false
18+
19+
[*.adoc]
20+
trim_trailing_whitespace = false
21+
22+
# Per-language indent widths.
23+
[*.rs]
24+
indent_size = 4
25+
26+
[*.zig]
27+
indent_size = 4
28+
29+
[*.{ada,adb,ads}]
30+
indent_size = 3
31+
32+
[*.{ex,exs}]
33+
indent_size = 2
34+
35+
[*.hs]
36+
indent_size = 2
37+
38+
[*.{res,resi}]
39+
indent_size = 2
40+
41+
[*.ncl]
42+
indent_size = 2
43+
44+
[*.rkt]
45+
indent_size = 2
46+
47+
[*.scm]
48+
indent_size = 2
49+
50+
[*.nix]
51+
indent_size = 2
52+
53+
[{Justfile,justfile}]
54+
indent_style = space
55+
indent_size = 4

.github/workflows/rust-ci.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
# Rust CI — thin wrapper calling the shared estate reusable in
33
# hyperpolymath/standards. Configure once, propagate everywhere.
44
# See: docs/CI-REUSABLE-WORKFLOWS.adoc in standards.
5+
#
6+
# DO NOT reintroduce a job-level `if: hashFiles('Cargo.toml') != ''` guard here.
7+
# Job-level `if:` is evaluated server-side before any checkout, so hashFiles()
8+
# has no workspace to hash. This workflow carried that guard on both local jobs
9+
# and, as a result, failed 60/60 runs at 0s with zero jobs and no logs — a
10+
# startup failure, not a test failure. GitHub reported the workflow's name as
11+
# `.github/workflows/rust-ci.yml` (the path-fallback used when a file has never
12+
# been parsed) rather than the declared `Rust CI`, which is how it was found.
13+
# Net effect: this repo's Rust build/test gate never ran between 2026-06-27 and
14+
# 2026-07-27. If a Cargo.toml presence check is ever needed, follow the estate
15+
# reusable and gate on a `detect` job output, not on hashFiles() at job level.
516
name: Rust CI
617

718
on:
@@ -24,7 +35,6 @@ jobs:
2435
name: Cargo build + test (ephapax-cli, --no-default-features)
2536
runs-on: ubuntu-latest
2637
timeout-minutes: 20
27-
if: hashFiles('Cargo.toml') != ''
2838

2939
# Proves ephapax can be built and tested with zero git dep on the
3040
# sibling `hyperpolymath/typed-wasm` repo. Enforces the estate
@@ -64,7 +74,6 @@ jobs:
6474
name: wasm-tools validate (emitted modules)
6575
runs-on: ubuntu-latest
6676
timeout-minutes: 20
67-
if: hashFiles('Cargo.toml') != ''
6877

6978
# Structurally validates every module ephapax emits for the fixture
7079
# corpus (`just validate-wasm`), catching codegen that produces an

.gitignore

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,36 @@ dist/
9797
*.cmi
9898
*.cmo
9999
*.cmx
100+
101+
# Additional build/tooling artefacts (salvaged from the 2026-07-26 sweep;
102+
# `.claude/` and `.editorconfig` were dropped from that set — both are TRACKED
103+
# in this repo and ignoring them would hide real changes from `git status`).
104+
*.ali
105+
*.chpl.tmp.*
106+
*.db
107+
*.db-journal
108+
*.db-shm
109+
*.db-wal
110+
*.ez
111+
*.jl.cov
112+
*.jl.mem
113+
*.py[cod]
114+
.venv/
115+
/.elixir_ls/
116+
/.stack-work/
117+
/Manifest.toml
118+
/_build/
119+
/bin/
120+
/build/
121+
/cover/
122+
/dist-newstyle/
123+
/dist/
124+
/doc/
125+
/exports/*.json
126+
/exports/*.lgt
127+
/obj/
128+
/out/
129+
__pycache__/
130+
composer/*.beam
131+
composer/build/
132+
erl_crash.dump

ephapax-linear/src/tests.rs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -422,19 +422,29 @@ fn p2p_contraction_always_rejected_both_disciplines() {
422422

423423
let mut linear = LinearChecker::new();
424424
let result = linear.check(&expr);
425-
assert!(result.is_err(), "P2P linear[{i}]: double use must be rejected");
425+
assert!(
426+
result.is_err(),
427+
"P2P linear[{i}]: double use must be rejected"
428+
);
426429
let violations = result.unwrap_err();
427430
assert!(
428-
violations.iter().any(|v| matches!(v, DisciplineViolation::Contraction { .. })),
431+
violations
432+
.iter()
433+
.any(|v| matches!(v, DisciplineViolation::Contraction { .. })),
429434
"P2P linear[{i}]: must report Contraction violation"
430435
);
431436

432437
let mut affine = AffineChecker::new();
433438
let result = affine.check(&expr);
434-
assert!(result.is_err(), "P2P affine[{i}]: double use must be rejected");
439+
assert!(
440+
result.is_err(),
441+
"P2P affine[{i}]: double use must be rejected"
442+
);
435443
let violations = result.unwrap_err();
436444
assert!(
437-
violations.iter().any(|v| matches!(v, DisciplineViolation::Contraction { .. })),
445+
violations
446+
.iter()
447+
.any(|v| matches!(v, DisciplineViolation::Contraction { .. })),
438448
"P2P affine[{i}]: must report Contraction violation"
439449
);
440450
}
@@ -459,10 +469,15 @@ fn p2p_weakening_linear_forbidden_affine_allowed() {
459469
// Linear must reject
460470
let mut linear = LinearChecker::new();
461471
let result = linear.check(&expr);
462-
assert!(result.is_err(), "P2P linear[{i}]: weakening must be rejected");
472+
assert!(
473+
result.is_err(),
474+
"P2P linear[{i}]: weakening must be rejected"
475+
);
463476
let violations = result.unwrap_err();
464477
assert!(
465-
violations.iter().any(|v| matches!(v, DisciplineViolation::WeakeningForbidden { .. })),
478+
violations
479+
.iter()
480+
.any(|v| matches!(v, DisciplineViolation::WeakeningForbidden { .. })),
466481
"P2P linear[{i}]: must report WeakeningForbidden"
467482
);
468483

@@ -790,7 +805,10 @@ fn aspect_checker_stateless_between_instances() {
790805
});
791806

792807
let mut checker1 = LinearChecker::new();
793-
assert!(checker1.check(&bad_expr).is_err(), "Aspect stateless: bad expr must fail");
808+
assert!(
809+
checker1.check(&bad_expr).is_err(),
810+
"Aspect stateless: bad expr must fail"
811+
);
794812

795813
// Second call with a DIFFERENT checker instance on a good expression
796814
let good_expr = e(ExprKind::LetLin {
@@ -835,10 +853,7 @@ fn aspect_violations_non_empty_on_failure() {
835853
body: Box::new(unit()),
836854
}),
837855
),
838-
(
839-
"drop",
840-
e(ExprKind::Drop(Box::new(i32_lit(3)))),
841-
),
856+
("drop", e(ExprKind::Drop(Box::new(i32_lit(3))))),
842857
];
843858

844859
for (label, expr) in &bad_cases {
@@ -1037,14 +1052,30 @@ fn aspect_violation_error_warning_mutually_exclusive() {
10371052
use crate::DisciplineViolation;
10381053

10391054
let all_violations: Vec<DisciplineViolation> = vec![
1040-
DisciplineViolation::Contraction { name: "x".to_string() },
1041-
DisciplineViolation::NotInScope { name: "y".to_string() },
1042-
DisciplineViolation::WeakeningForbidden { name: "z".to_string() },
1055+
DisciplineViolation::Contraction {
1056+
name: "x".to_string(),
1057+
},
1058+
DisciplineViolation::NotInScope {
1059+
name: "y".to_string(),
1060+
},
1061+
DisciplineViolation::WeakeningForbidden {
1062+
name: "z".to_string(),
1063+
},
10431064
DisciplineViolation::DropForbidden,
1044-
DisciplineViolation::BranchDisagreement { name: "b".to_string() },
1045-
DisciplineViolation::RegionLeakLinear { region: "r".to_string(), name: "s".to_string() },
1046-
DisciplineViolation::LetForLinearType { name: "t".to_string() },
1047-
DisciplineViolation::ImplicitDropWarning { region: "r".to_string(), name: "u".to_string() },
1065+
DisciplineViolation::BranchDisagreement {
1066+
name: "b".to_string(),
1067+
},
1068+
DisciplineViolation::RegionLeakLinear {
1069+
region: "r".to_string(),
1070+
name: "s".to_string(),
1071+
},
1072+
DisciplineViolation::LetForLinearType {
1073+
name: "t".to_string(),
1074+
},
1075+
DisciplineViolation::ImplicitDropWarning {
1076+
region: "r".to_string(),
1077+
name: "u".to_string(),
1078+
},
10481079
];
10491080

10501081
for violation in &all_violations {

src/ephapax-cli/src/import_resolver.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,7 @@ fn first_module_declaration(source: &str) -> Option<String> {
151151
let rest = rest.trim();
152152
// Take everything up to a whitespace, comma, or comment marker.
153153
let end = rest
154-
.find(|c: char| {
155-
!(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/')
156-
})
154+
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/'))
157155
.unwrap_or(rest.len());
158156
let name = &rest[..end];
159157
if !name.is_empty() {
@@ -208,15 +206,14 @@ fn visit(
208206
message: e.to_string(),
209207
})?;
210208

211-
let surface =
212-
parse_surface_module(&source, logical).map_err(|errs| ResolveError::Parse {
213-
path: file_path.clone(),
214-
message: errs
215-
.iter()
216-
.map(|e| format!("{}", e))
217-
.collect::<Vec<_>>()
218-
.join("; "),
219-
})?;
209+
let surface = parse_surface_module(&source, logical).map_err(|errs| ResolveError::Parse {
210+
path: file_path.clone(),
211+
message: errs
212+
.iter()
213+
.map(|e| format!("{}", e))
214+
.collect::<Vec<_>>()
215+
.join("; "),
216+
})?;
220217

221218
// Recurse into imports BEFORE inserting this module so that the
222219
// post-order visit places dependencies before this module.
@@ -226,7 +223,9 @@ fn visit(
226223
.map(|i| normalise_path(i.module.as_str()))
227224
.collect();
228225
for dep in &deps {
229-
visit(dep, None, base_dir, mod_index, loaded, order, visiting, stack)?;
226+
visit(
227+
dep, None, base_dir, mod_index, loaded, order, visiting, stack,
228+
)?;
230229
}
231230

232231
loaded.insert(
@@ -253,9 +252,9 @@ fn root_module_path_from_source(root_path: &Path) -> Result<String, ResolveError
253252
let line = line.trim_start();
254253
if let Some(rest) = line.strip_prefix("module") {
255254
let rest = rest.trim();
256-
if let Some(end) = rest.find(|c: char| {
257-
!(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/')
258-
}) {
255+
if let Some(end) = rest
256+
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/'))
257+
{
259258
return Ok(normalise_path(&rest[..end]));
260259
} else if !rest.is_empty() {
261260
return Ok(normalise_path(rest));

src/ephapax-cli/src/main.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,16 @@ fn compile_file(
474474
if verbose {
475475
println!("{} Type check passed", "✓".green());
476476
}
477-
return finish_compile(module, &filename, path, output, opt_level, debug,
478-
verify_ownership, verbose);
477+
return finish_compile(
478+
module,
479+
&filename,
480+
path,
481+
output,
482+
opt_level,
483+
debug,
484+
verify_ownership,
485+
verbose,
486+
);
479487
}
480488
Err(e) => return Err(e.to_string()),
481489
};
@@ -548,8 +556,16 @@ fn compile_file(
548556
println!("{} Type check passed", "✓".green());
549557
}
550558

551-
finish_compile(module, &filename, path, output, opt_level, debug,
552-
verify_ownership, verbose)
559+
finish_compile(
560+
module,
561+
&filename,
562+
path,
563+
output,
564+
opt_level,
565+
debug,
566+
verify_ownership,
567+
verbose,
568+
)
553569
}
554570

555571
/// Codegen + optional ownership verification + output write. Shared by
@@ -564,7 +580,6 @@ fn finish_compile(
564580
verify_ownership: bool,
565581
verbose: bool,
566582
) -> Result<(), String> {
567-
568583
// Compile to WASM (with or without debug info)
569584
let wasm_bytes = if debug {
570585
ephapax_wasm::compile_module(&module).map_err(|e| format!("Codegen error: {}", e))?
@@ -672,16 +687,11 @@ fn report_ownership_verification(wasm_bytes: &[u8], verbose: bool) -> Result<(),
672687
match verify_from_module(wasm_bytes) {
673688
Ok(()) => {
674689
if verbose {
675-
println!(
676-
"{} typed-wasm L7+L10 verification: clean",
677-
"✓".green()
678-
);
690+
println!("{} typed-wasm L7+L10 verification: clean", "✓".green());
679691
}
680692
Ok(())
681693
}
682-
Err(VerifyError::Parse(e)) => {
683-
Err(format!("verifier could not parse emitted wasm: {}", e))
684-
}
694+
Err(VerifyError::Parse(e)) => Err(format!("verifier could not parse emitted wasm: {}", e)),
685695
Err(VerifyError::Ownership(errs)) => {
686696
eprintln!(
687697
"{} typed-wasm verification failed: {} violation(s)",

0 commit comments

Comments
 (0)