Skip to content

Commit 2d35425

Browse files
feat(chapel): wire MassPanic CI + cross-rust contract checks (refs #33) (#85)
## Summary Lands six strict CI gates on the **optional, detachable** Chapel mass-panic harness and the Chapel↔Rust CLI contract, plus audit-surfaced bug fixes in `Imaging.chpl`, `MassPanic.chpl`, and the chapel/README. **Detachability stays intact**: the Rust binary is still standalone, USB-stick-portable, single-machine. Removing `chapel/` leaves Cargo green. The new `panic-attack describe-contract` subcommand is framed as a generic orchestrator capability (Nextflow / Airflow / Slurm / shell scripts / Chapel) — not a Chapel bridge. Closes the producer side of #33's hexad contract: any future drift between Chapel's writers and the Rust hexad schema is caught by `chapel-rust-diff` (aggregates) or `chapel-cli-contract` (CLI shape). ## Six strict gates (no `continue-on-error` anywhere) | Gate | What it asserts | |------|------------------| | `chapel-parse-check` | `chpl --parse-only` on every module + smoke | | `chapel-build` | `just chapel-build-ci` (no toolbox; stock ubuntu .deb) | | `chapel-smoke` | `RepoResult → SystemImage → JSON` data-flow + the four silent-loss fixes | | `chapel-e2e` | mass-panic full pipeline `--numLocales=1` on synthetic 2-repo corpus | | `chapel-cli-contract` | `panic-attack describe-contract` vs `chapel/tests/expected_contract.json` | | `chapel-rust-diff` | rayon assemblyline vs Chapel single-locale aggregate parity | ## Bug fixes included (audit-surfaced) * **Imaging.chpl::writeNodeJson** — emit `path`, `high_count`, `error`, `category_breakdown`. Four `ImageNode` fields were populated in memory but silently dropped from `SystemImage` JSON. JSON-escape helper added. * **MassPanic.chpl::selectAndAnnounceScheduler** — combining `--scheduler=static` with `--resume` now hard-fails (was: warning then silently ran without resume support). * **MassPanic.chpl::buildCommandArgs** — adjudicate mode passes `--quiet` for consistency with assail/assault/ambush (banner-bleed risk into Chapel's JSON parser). * **MassPanic.chpl::journalEscape** — rename local `out` → `buf` (Chapel 2.8.0 parses `out` as the intent keyword more strictly than older versions; the local var actually blocked compilation). * **chapel/README.md** — Chapel version pin aligned to 2.8.0+ (was drifted 2.3.0+), fNIRS terminology table cross-referenced, scheduler perf claim qualified as "estimate pending Wave 2 benchmark", `--resume` hard-fail documented. * **Justfile chapel-scan** — fixed a pre-existing just parse error (`env(\"HOME\") + \"...\"` needed parens) that was blocking every `just <chapel-*>` invocation. ## Modes exercised Chapel mass-panic invokes the Rust binary in five modes; all five are covered by `chapel-cli-contract`: `assail` · `assault` · `ambush` · `attack` · `adjudicate`. The new `describe-contract` subcommand introspects clap at runtime via `CommandFactory`, so adding a new flag to any mode auto-appears in the contract output. Drift demo: add a synthetic flag to one of the modes in `src/main.rs`, the `chapel-cli-contract` gate fails on the next CI run. ## Rayon vs Chapel diff (local, before/after) Before this PR: no comparison existed. After this PR, on the synthetic 2-repo corpus the gate uses: ``` rayon_vs_chapel_diff: ok [total_weak_points] = 3 rayon_vs_chapel_diff: ok [total_critical] = 0 rayon_vs_chapel_diff: ok [repos_scanned] = 2 rayon_vs_chapel_diff: PASS — rayon and Chapel single-locale aggregates agree. ``` ## Local verification ``` $ chpl --version chpl version 2.8.0 $ cargo build --release # ok $ just chapel-parse-check # ok $ just chapel-build-ci # mass-panic + smoke built $ just chapel-smoke # PASS (4 silent-loss fields + JSON-escape) $ just chapel-e2e # PASS (-nl 1 writes system-image JSON) $ just chapel-contract-check # PASS (5 modes, 2 globals, 5 per-mode flags) $ just chapel-rust-diff # PASS (rayon=3, chapel=3) ``` ## Wave 2 deferred items (tracked in the ADR) * **True multi-locale CI** — needs `CHPL_COMM=gasnet`; stock ubuntu .deb ships `CHPL_COMM=none`. The v0 chapel-e2e job uses `-nl 1`. * **Subprocess hang kill path** — `panic-attack --timeout=N` is the only safeguard; Chapel `sub.wait()` blocks indefinitely if the subprocess ignores the timeout. * **SHA-pin the Chapel .deb download** — workflow currently trusts HTTPS endpoint at `chapel-lang/chapel` releases. Add SHA256 verification before promoting Chapel to a production gate. * **BoJ-estate scheduler benchmark** — back the README's "~5–15% slower" claim with measured numbers on a 350-repo corpus. * **NFS journal lock semantics** — document POSIX-flock requirement + add a startup probe. * **`buildCommandArgs` callsite consolidation** — three subprocess sites bypass the canonical builder. ## Docs * `docs/adr/0001-chapel-distributed-scanner.md` — decision record. * `docs/adr/0001-chapel-issue-33-comment.md` — draft cross-link to be posted on #33 after merge. ## Test plan - [x] `cargo build --release` (locally green) - [x] Chapel modules parse + build with stock Chapel 2.8.0 - [x] Six gates pass locally (Chapel 2.8.0 + cargo 1.95.0) - [ ] All six gates green in chapel-ci.yml on this PR - [ ] Watch `rust-ci.yml` / `cargo-audit.yml` / other gates unaffected (detachability check — they shouldn't trigger on chapel/** changes, but they will trigger on src/main.rs + Cargo.lock changes) - [ ] Post comment on #33 after merge - [ ] File Wave 2 cluster-validation tracker after merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e32818a commit 2d35425

13 files changed

Lines changed: 920 additions & 22 deletions

.github/workflows/chapel-ci.yml

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
#
3+
# chapel-ci — strict CI gates for the OPTIONAL Chapel mass-panic harness.
4+
#
5+
# The Rust binary stands alone (USB-stick-portable, single-machine). chapel/ is
6+
# a detachable multi-machine harness on top. This workflow exercises the harness
7+
# and the Rust↔Chapel contract surface. Path triggers are scoped so a pure-Rust
8+
# PR that doesn't touch chapel/ or src/main.rs leaves these jobs unrun, and
9+
# removing chapel/ entirely leaves the Rust CI path (rust-ci.yml) green.
10+
#
11+
# Six strict jobs (no continue-on-error):
12+
# 1. chapel-parse-check — chpl --parse-only on every module
13+
# 2. chapel-build — just chapel-build-ci (no toolbox)
14+
# 3. chapel-smoke — chapel/smoke/two_repo_smoke (Chapel data flow)
15+
# 4. chapel-e2e — mass-panic end-to-end (-nl 1) on a synthetic
16+
# 2-repo manifest. True -nl 2 requires CHPL_COMM=gasnet
17+
# which the stock .deb doesn't ship; tracked for Wave 2.
18+
# 5. chapel-cli-contract — assert panic-attack describe-contract matches fixture
19+
# 6. chapel-rust-diff — rayon assemblyline vs Chapel single-locale aggregates
20+
#
21+
# Wave 2 hardening tracker: SHA-pin the Chapel 2.8.0 .deb download. Today the
22+
# workflow trusts the HTTPS endpoint at chapel-lang/chapel releases. Acceptable
23+
# for the harness scaffold; harden before promoting Chapel to a production gate.
24+
25+
name: chapel-ci
26+
27+
on:
28+
push:
29+
branches: [main]
30+
paths:
31+
- 'chapel/**'
32+
- 'Justfile'
33+
- '.github/workflows/chapel-ci.yml'
34+
- 'src/main.rs'
35+
- 'src/types.rs'
36+
- 'Cargo.toml'
37+
- 'Cargo.lock'
38+
pull_request:
39+
paths:
40+
- 'chapel/**'
41+
- 'Justfile'
42+
- '.github/workflows/chapel-ci.yml'
43+
- 'src/main.rs'
44+
- 'src/types.rs'
45+
- 'Cargo.toml'
46+
- 'Cargo.lock'
47+
48+
permissions:
49+
contents: read
50+
51+
concurrency:
52+
group: chapel-ci-${{ github.workflow }}-${{ github.ref }}
53+
cancel-in-progress: true
54+
55+
env:
56+
CHAPEL_VERSION: "2.8.0"
57+
CHAPEL_DEB_URL: "https://github.com/chapel-lang/chapel/releases/download/2.8.0/chapel-2.8.0-1.ubuntu22.amd64.deb"
58+
59+
jobs:
60+
chapel-parse-check:
61+
name: chapel-parse-check
62+
runs-on: ubuntu-22.04
63+
steps:
64+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
65+
- name: Install just
66+
run: sudo apt-get update -qq && sudo apt-get install -y just
67+
- name: Install Chapel ${{ env.CHAPEL_VERSION }}
68+
run: |
69+
set -euo pipefail
70+
curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}"
71+
sudo apt-get install -y /tmp/chapel.deb
72+
chpl --version
73+
- name: Parse every Chapel module
74+
run: just chapel-parse-check
75+
76+
chapel-build:
77+
name: chapel-build
78+
needs: chapel-parse-check
79+
runs-on: ubuntu-22.04
80+
steps:
81+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
82+
- name: Install just
83+
run: sudo apt-get update -qq && sudo apt-get install -y just
84+
- name: Install Chapel ${{ env.CHAPEL_VERSION }}
85+
run: |
86+
set -euo pipefail
87+
curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}"
88+
sudo apt-get install -y /tmp/chapel.deb
89+
chpl --version
90+
- name: Build mass-panic + smoke (no toolbox)
91+
run: just chapel-build-ci
92+
- name: Upload Chapel artefacts
93+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
94+
with:
95+
name: chapel-binaries
96+
path: |
97+
chapel/mass-panic
98+
chapel/smoke/two_repo_smoke
99+
retention-days: 1
100+
101+
chapel-smoke:
102+
name: chapel-smoke
103+
needs: chapel-build
104+
runs-on: ubuntu-22.04
105+
steps:
106+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
107+
- name: Install Chapel ${{ env.CHAPEL_VERSION }}
108+
run: |
109+
set -euo pipefail
110+
curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}"
111+
sudo apt-get install -y /tmp/chapel.deb
112+
- name: Download Chapel artefacts
113+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
114+
with:
115+
name: chapel-binaries
116+
path: chapel/
117+
- name: Restore exec bits
118+
run: chmod +x chapel/mass-panic chapel/smoke/two_repo_smoke
119+
- name: Run two_repo_smoke
120+
run: ./chapel/smoke/two_repo_smoke
121+
122+
chapel-e2e:
123+
name: chapel-e2e
124+
needs: chapel-build
125+
runs-on: ubuntu-22.04
126+
steps:
127+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
128+
- name: Install just + Chapel ${{ env.CHAPEL_VERSION }}
129+
run: |
130+
set -euo pipefail
131+
sudo apt-get update -qq && sudo apt-get install -y just
132+
curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}"
133+
sudo apt-get install -y /tmp/chapel.deb
134+
- name: Download Chapel artefacts
135+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
136+
with:
137+
name: chapel-binaries
138+
path: chapel/
139+
- name: Restore exec bits
140+
run: chmod +x chapel/mass-panic
141+
- name: End-to-end -nl 1 exercise
142+
run: just chapel-e2e
143+
144+
chapel-cli-contract:
145+
name: chapel-cli-contract
146+
runs-on: ubuntu-22.04
147+
steps:
148+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
149+
- uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
150+
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
151+
with:
152+
path: |
153+
~/.cargo/registry
154+
~/.cargo/git
155+
target
156+
key: ${{ runner.os }}-cargo-chapel-cli-contract-${{ hashFiles('Cargo.lock') }}
157+
- name: Build panic-attack
158+
run: cargo build --release --locked
159+
- name: Run contract gate
160+
run: ./chapel/tests/contract_check.sh
161+
162+
chapel-rust-diff:
163+
name: chapel-rust-diff
164+
needs: chapel-build
165+
runs-on: ubuntu-22.04
166+
steps:
167+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
168+
- uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
169+
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
170+
with:
171+
path: |
172+
~/.cargo/registry
173+
~/.cargo/git
174+
target
175+
key: ${{ runner.os }}-cargo-chapel-rust-diff-${{ hashFiles('Cargo.lock') }}
176+
- name: Install Chapel ${{ env.CHAPEL_VERSION }}
177+
run: |
178+
set -euo pipefail
179+
curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}"
180+
sudo apt-get install -y /tmp/chapel.deb
181+
- name: Download Chapel artefacts
182+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
183+
with:
184+
name: chapel-binaries
185+
path: chapel/
186+
- name: Restore exec bits
187+
run: chmod +x chapel/mass-panic
188+
- name: Build panic-attack
189+
run: cargo build --release --locked
190+
- name: rayon vs Chapel single-locale aggregate parity
191+
run: ./chapel/tests/rayon_vs_chapel_diff.sh

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ htmlcov/
106106
# asdf version manager
107107
.tool-versions
108108
chapel/mass-panic
109+
chapel/smoke/two_repo_smoke
109110
target/
110111
node_modules/
111112
_build/

Justfile

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,58 @@ chapel-build:
144144
chapel-build-toolbox:
145145
toolbox run --container chapel-dev bash -c "cd $(pwd)/chapel && chpl src/MassPanic.chpl src/Protocol.chpl src/Imaging.chpl src/Temporal.chpl -o mass-panic"
146146

147+
# CI build path — explicitly toolbox-free. Used by .github/workflows/chapel-ci.yml
148+
# on stock ubuntu-latest runners after installing the Chapel 2.8.0 .deb. Builds
149+
# mass-panic and the smoke binary in one shot.
150+
chapel-build-ci:
151+
cd chapel && chpl src/MassPanic.chpl src/Protocol.chpl src/Imaging.chpl src/Temporal.chpl -o mass-panic
152+
cd chapel && chpl smoke/two_repo_smoke.chpl src/Protocol.chpl src/Imaging.chpl -o smoke/two_repo_smoke
153+
154+
# Chapel-side data-flow smoke test (<5s, single-locale, no Rust binary needed)
155+
chapel-smoke: chapel-build-ci
156+
./chapel/smoke/two_repo_smoke
157+
158+
# Verify the Rust↔Chapel CLI contract surface. Requires
159+
# `cargo build --release` to have produced ./target/release/panic-attack.
160+
chapel-contract-check:
161+
./chapel/tests/contract_check.sh
162+
163+
# Aggregate-parity check: rayon assemblyline vs Chapel single-locale on a
164+
# synthetic 2-repo corpus. Requires both binaries built.
165+
chapel-rust-diff:
166+
./chapel/tests/rayon_vs_chapel_diff.sh
167+
168+
# End-to-end single-locale exercise against a synthetic 2-repo manifest.
169+
#
170+
# True multi-locale (-nl 2 over real cluster nodes) requires Chapel built with
171+
# CHPL_COMM=gasnet — the stock ubuntu .deb ships CHPL_COMM=none and rejects
172+
# -nl >1. The v0 gate verifies the full mass-panic flow (discover → spawn
173+
# panic-attack → write SystemImage JSON) on -nl 1; the cross-locale code path
174+
# is staged for Wave 2 once the .deb story for multilocale Chapel is solved.
175+
# Tracker: see chapel/README.md "Wave 2 follow-up".
176+
chapel-e2e:
177+
#!/usr/bin/env bash
178+
set -euo pipefail
179+
WORK=$(mktemp -d /tmp/chapel-e2e-XXXXXX)
180+
trap 'rm -rf "$WORK"' EXIT
181+
mkdir -p "$WORK/corpus/repo-alpha/src" "$WORK/corpus/repo-beta/src"
182+
echo 'pub unsafe fn a() {}' > "$WORK/corpus/repo-alpha/src/lib.rs"
183+
echo 'pub unsafe fn b() {}' > "$WORK/corpus/repo-beta/src/lib.rs"
184+
for d in repo-alpha repo-beta; do (cd "$WORK/corpus/$d" && git init -q && git add -A && git -c user.email=ci@example.com -c user.name=ci commit -q -m init); done
185+
./chapel/mass-panic --repoDirectory="$WORK/corpus" --numLocales=1 --quiet --outputDir="$WORK/out"
186+
ls "$WORK/out"/system-image-*.json >/dev/null && echo "chapel-e2e: PASS (-nl 1 produced system-image JSON)"
187+
188+
# Parse-check every Chapel module (cheap canary; runs before chapel-build-ci)
189+
chapel-parse-check:
190+
cd chapel && chpl --parse-only src/MassPanic.chpl src/Protocol.chpl src/Imaging.chpl src/Temporal.chpl
191+
cd chapel && chpl --parse-only smoke/two_repo_smoke.chpl src/Protocol.chpl src/Imaging.chpl
192+
147193
# Clean Chapel build artefacts
148194
chapel-clean:
149-
rm -f chapel/mass-panic
195+
rm -f chapel/mass-panic chapel/smoke/two_repo_smoke
150196

151197
# Scan local repo tree with mass-panic (Chapel single-locale)
152-
chapel-scan dir=env("HOME") + "/Documents/hyperpolymath-repos":
198+
chapel-scan dir=(env("HOME") + "/Documents/hyperpolymath-repos"):
153199
./chapel/mass-panic --repoDirectory={{dir}}
154200

155201
# Diff the two most recent mass-panic temporal snapshots

chapel/README.md

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Locale 0 (coordinator) Locale 1..N (workers)
2121

2222
## Prerequisites
2323

24-
- [Chapel](https://chapel-lang.org/) 2.3.0+
24+
- [Chapel](https://chapel-lang.org/) 2.8.0+ (matches `chapel/Mason.toml`)
2525
- `panic-attack` binary on PATH (or specify via `--panicAttackBin`)
2626

2727
## Build
@@ -70,7 +70,7 @@ chpl src/MassPanic.chpl src/Protocol.chpl src/Imaging.chpl src/Temporal.chpl -o
7070
| `--panicAttackBin` | `panic-attack` | Path to panic-attack binary |
7171
| `--mode` | `assail` | Operation mode (see above) |
7272
| `--scheduler` | `static` | `static` (fast, not resumable) or `queue` (resumable, ~5–15% slower) |
73-
| `--resume` | `false` | Only with `--scheduler=queue`: skip repos already marked "done" in the journal |
73+
| `--resume` | `false` | Requires `--scheduler=queue`; combining with `--scheduler=static` exits with an error (static mode has no journal). Skips repos already marked "done" in the journal |
7474
| `--journalDir` | `<outputDir>/journal` | Directory for queue-scheduler JSONL shards |
7575
| `--incremental` | `true` | Skip unchanged repos via BLAKE3 |
7676
| `--cacheFile` | | Fingerprint cache file path |
@@ -128,10 +128,12 @@ previously-completed repos and the freshly-scanned ones.
128128
invocation with `--resume` reuses everything completed so far.
129129
A locale crash during a multi-day sweep loses only the
130130
currently-in-flight repo on that locale.
131-
- **~5–15% slower** on clean runs. The dispatch overhead per task
132-
(atomic fetch-add + one journal write) is per-repo instead of
133-
being amortised across a `coforall` range. On a clean 10k-repo
134-
sweep, expect queue mode to finish in ~1.10× the time of static.
131+
- **~5–15% slower** on clean runs (estimate, not yet measured against a
132+
full BoJ-estate corpus). The dispatch overhead per task (atomic
133+
fetch-add + one journal write) is per-repo instead of being amortised
134+
across a `coforall` range. On a clean 10k-repo sweep, expect queue
135+
mode to finish in roughly ~1.10× the time of static. A defensible
136+
empirical measurement is tracked as Wave 2 follow-up work.
135137
- **Right for:** long interactive sweeps (GitHub-account scale or
136138
larger), sweeps where at least one locale is on spot/preemptible
137139
infrastructure, or any run where you expect to want to pause
@@ -193,7 +195,8 @@ The banner is suppressed under `--quiet`.
193195

194196
## Relationship to Rust assemblyline
195197

196-
The Chapel layer is **optional**. For single-machine scanning, use:
198+
The Chapel layer is **optional** — a detachable harness on top of the
199+
standalone Rust binary. For single-machine scanning, use:
197200

198201
```bash
199202
panic-attack assemblyline /path/to/repos # rayon parallel
@@ -202,4 +205,32 @@ panic-attack image /path/to/repos # + imaging + temporal
202205

203206
Chapel adds multi-machine distribution for scanning at GitHub-account or
204207
datacenter scale, where hundreds of machines each scan their partition of
205-
repositories simultaneously.
208+
repositories simultaneously. Removing `chapel/` entirely leaves the Rust
209+
build green and the single-machine USB-stick experience intact.
210+
211+
The Chapel↔Rust contract is exposed via `panic-attack describe-contract`
212+
(introduced for the chapel-cli-contract CI gate). Any external orchestrator
213+
— Chapel mass-panic, Nextflow, Airflow, Slurm, a hand-rolled shell script —
214+
can call it to discover accepted flags per mode and the report
215+
`schema_version` without coupling itself to panic-attack source.
216+
217+
## Neuroscience analogy: fNIRS-inspired imaging
218+
219+
panic-attack applies functional Near-Infrared Spectroscopy (fNIRS) concepts
220+
to codebase health mapping. The canonical mapping lives in
221+
[`src/Imaging.chpl`](src/Imaging.chpl) header (lines 4-27) and is mirrored
222+
here so the metaphor doesn't drift:
223+
224+
| fNIRS term | panic-attack equivalent |
225+
|-----------------------|-------------------------------------------------------|
226+
| Cortical region | Repository / directory / file |
227+
| Blood oxygenation | Health score (inverse of risk) |
228+
| Neural activation | Weak point density (findings per KLOC) |
229+
| Hemodynamic response | Change velocity (how fast risk is changing) |
230+
| Optode placement | Scanner coverage (which files were analysed) |
231+
| Channel | Dependency / taint flow edge |
232+
| Functional map | `SystemImage` |
233+
| Time series | Temporal snapshot sequence in VeriSimDB |
234+
235+
When a new health metric is added, update both `Imaging.chpl` and this
236+
table; CI does not enforce the mapping but reviewers should.

0 commit comments

Comments
 (0)