nigig-org/.forgejo/workflows/cad.yml
andodeki 3cfcc2cc60 fix(cad-core): CORE-04 remove last runtime fan triangulators; plan status to complete
- math::triangulate_polygon now ear-clips via polygon::triangulate_indices
  (index/orientation preserving; rejects degenerate, non-finite, bow-tie)
- extrude_polygon_mesh routes through triangulate_profile; new
  try_extrude_polygon_mesh returns structured errors instead of
  underflowing on undersized profiles
- tests: concave area equality, closed-manifold edge twins, bad corpus
- cad.yml gate forbids reintroducing the vertex-zero fan
- EXECUTION_PLAN.md: tranche ledger, verification, release gates
2026-09-26 12:05:51 +00:00

603 lines
30 KiB
YAML

name: cad
# CORE-00 — truthful CAD CI, owned by the CAD crates.
#
# Previously CAD gates lived inside `nigig-build.yml` aimed at
# `crates/apps/nigig-build/src/construction_frame/pages/workspace/cad`,
# a tree that no longer exists. A grep over a missing directory exits
# non-zero inside `if`, so every one of those gates reported "OK" while
# scanning nothing. This workflow scans the real trees
# (`crates/apps/cad/cad-core`, `crates/apps/cad/cad-ui`) and every gate
# below fails when its target set is empty -- an empty scan is never green.
#
# Known-red policy: the `cad-ui` demo semantic failure was fixed
# legitimately by the UI-02 kind_hint fix (see IGNORED_TESTS.md demo
# triage) and `cargo test -p cad-core` needs the pinned toolchain.
# What stays visible: any semantic failure must stay visible; this
# workflow must not be weakened to recover green.
on:
push:
paths:
- 'crates/apps/cad/**'
- 'tools/test-cad-coverage.sh'
- 'tools/test-cad-widget-coverage.sh'
- 'Cargo.lock'
- 'Cargo.toml'
- 'rust-toolchain.toml'
- '.forgejo/workflows/cad.yml'
- 'crates/apps/cad/cad-ui/IGNORED_TESTS.md'
pull_request:
paths:
- 'crates/apps/cad/**'
- 'tools/test-cad-coverage.sh'
- 'tools/test-cad-widget-coverage.sh'
- 'Cargo.lock'
- 'Cargo.toml'
- 'rust-toolchain.toml'
- '.forgejo/workflows/cad.yml'
- 'crates/apps/cad/cad-ui/IGNORED_TESTS.md'
# Explicit non-zero budget for ignored CAD tests. Bumping this number
# requires a tranche note with owner, reason, and expiry (UI-00 owns the
# inventory). A drift in either direction fails: silently adding ignores
# hides coverage, silently dropping the count means this budget is stale.
env:
CAD_IGNORED_BUDGET: 20
jobs:
# Fast gates, no toolchain. A red job here means the scan itself is
# dishonest -- fix the scan, never the target count.
cad-truth-gates:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
# Every source scan below must assert it scanned at least one owned
# file. This helper is the single definition of "non-empty"; the
# empty-fixture proof at the end of this job exercises it.
- name: CAD source roots are non-empty
run: |
set -euo pipefail
scan_rs() {
local dir="$1"
local n
n=$(find "$dir" -name '*.rs' | wc -l)
if [ "$n" -eq 0 ]; then
echo "ERROR: no Rust sources under $dir -- the scan target is empty."
return 1
fi
echo "OK: $dir ($n files)"
}
scan_rs crates/apps/cad/cad-core/src
scan_rs crates/apps/cad/cad-ui/src
# The removed tree must not be scanned as if it still existed. Each
# live reference needs a guard on the CAD variable/dir itself (added
# by CORE-00) until BUILD-00 removes the stale gates outright. The
# match is deliberately narrow: an unrelated `test -f` elsewhere in
# the file does not count as guarding the CAD scan.
- name: Stale CAD tree references fail closed
run: |
set -euo pipefail
stale='nigig-build/src/construction_frame/pages/workspace/cad'
fail=0
# cad.yml itself is excluded: it names the removed tree only to
# forbid scanning it, and never scans it. Comment-only lines are
# stripped like the nigig-build.yml gates do, so documentation
# cannot trip the gate.
while IFS= read -r ref; do
file="${ref%%:*}"
[ "$file" = ".forgejo/workflows/cad.yml" ] && continue
code="$(echo "$ref" | sed 's/^[^:]*:[0-9]*://;s/^[[:space:]]*//')"
case "$code" in \#*) continue ;; esac
if ! grep -qE 'test -d "\$cad"|test -f "\$f"|os\.path\.isdir\(CAD|\[\[ ! -d "\$CAD"|\[\[ ! -d "\$ROOT/\$CAD_REL"' "$file"; then
echo "UNGUARDED stale reference: $ref"
fail=1
fi
done < <(grep -rn --include='*.yml' --include='*.sh' "$stale" .forgejo/workflows tools || true)
if [ "$fail" -ne 0 ]; then
echo
echo "ERROR: the reference(s) above scan a removed tree with no"
echo "missing-dir guard, so the gate passes over an empty set."
echo "Guard it (fail closed) or remove it under BUILD-00."
exit 1
fi
echo "OK: all stale-tree references are guarded"
# Ignored tests are a budget, not background noise.
- name: Ignored-test budget is exact
run: |
set -euo pipefail
n=$(grep -rn '#\[ignore' crates/apps/cad/cad-core/src crates/apps/cad/cad-ui/src | wc -l)
if [ "$n" -ne "$CAD_IGNORED_BUDGET" ]; then
echo "ERROR: $n ignored CAD tests, budget is $CAD_IGNORED_BUDGET."
echo "Update CAD_IGNORED_BUDGET with a tranche note (owner, reason, expiry)."
exit 1
fi
echo "OK: ignored CAD tests = $n (budget $CAD_IGNORED_BUDGET)"
# UI-00: every true #[ignore] must be named in the inventory with
# owner, reason, and expiry. The budget gate above counts grep hits
# (20 = 19 attributes + 1 doc-comment mention); this gate checks
# the 19 attribute owners actually document their test.
- name: Ignore inventory names every ignored test
run: |
set -euo pipefail
inv=crates/apps/cad/cad-ui/IGNORED_TESTS.md
test -f "$inv" || { echo "ERROR: $inv is missing."; exit 1; }
fail=0
while IFS= read -r line; do
fn="$(echo "$line" | sed -n 's/.*fn \([A-Za-z0-9_]*\)(.*/\1/p')"
[ -n "$fn" ] || continue
if ! grep -q "$fn" "$inv"; then
echo "UNINVENTORIED ignored test: $fn ($line)"
fail=1
fi
done < <(grep -rn -A1 '#\[ignore' crates/apps/cad/cad-core/src crates/apps/cad/cad-ui/src | grep -E 'fn [A-Za-z0-9_]+\(' || true)
if [ "$fail" -ne 0 ]; then
echo
echo "ERROR: ignored test(s) above are not named in $inv."
echo "Add owner, reason, and expiry there in the same tranche."
exit 1
fi
echo "OK: all ignored test fns are inventoried"
# UI-00: an expiry in the past fails. Extensions are reviewable
# edits to IGNORED_TESTS.md, never silent CI edits. Only the
# pipe-table expiry column is scanned, so the header date never
# trips the gate.
- name: No ignore expiry has passed
run: |
set -euo pipefail
inv=crates/apps/cad/cad-ui/IGNORED_TESTS.md
today=$(date +%F)
fail=0
while IFS= read -r d; do
d="$(echo "$d" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}')"
if [[ "$d" < "$today" ]]; then
echo "EXPIRED ignore entry: $d (today is $today)"
fail=1
fi
done < <(grep -E '^\| [0-9]+ \|' "$inv" | grep -oE '\| [0-9]{4}-[0-9]{2}-[0-9]{2}' || true)
if [ "$fail" -ne 0 ]; then
echo
echo "ERROR: at least one ignore expiry has passed."
echo "Re-triage, convert to a nightly/device job, or extend with reason."
exit 1
fi
echo "OK: no ignore expiry has passed (today $today)"
# UI-00 demo triage, fixed legitimately by the UI-02 kind_hint
# fix: demo_has_slab_and_wall failed because domain_box() never
# set kind_hint, so wall+slab both classified as Cube and
# demo_counts() returned (0,0). The test stays as a regression
# guard; this gate forbids hiding it by deletion, inversion, or
# #[ignore].
- name: Demo semantic failure is preserved
run: |
set -euo pipefail
demo=crates/apps/cad/cad-ui/src/demo.rs
test -f "$demo" || { echo "ERROR: $demo is missing."; exit 1; }
grep -q 'fn demo_has_slab_and_wall' "$demo" \
|| { echo "ERROR: demo_has_slab_and_wall test was deleted."; exit 1; }
grep -q 'assert!(walls >= 1' "$demo" \
|| { echo "ERROR: walls >= 1 assertion was removed or inverted."; exit 1; }
grep -q 'assert!(slabs >= 1' "$demo" \
|| { echo "ERROR: slabs >= 1 assertion was removed or inverted."; exit 1; }
if grep -q -B3 'fn demo_has_slab_and_wall' "$demo" | grep -q '#\[ignore'; then
echo "ERROR: demo failure was hidden behind #[ignore]."
exit 1
fi
echo "OK: demo semantic test is preserved (regression guard)"
# UI-01 capability containment: STEP, F12 capture, render2d, and the
# RayTrace shading slot are contained by the matrix in
# `cad-ui/src/capabilities.rs`. Every sub-check below fails with the
# file that reintroduces a reachable misleading action. Like the
# UI-00 gates, these are static (no toolchain): they pin the exact
# labels, predicates, and dispatch paths one matrix row owns.
- name: UI-01 contained capabilities stay contained
run: |
set -euo pipefail
ui=crates/apps/cad/cad-ui/src
fail=0
say() { echo "$1"; fail=1; }
# 1. The matrix exists and names every contained capability once.
for cap in StepExport ImageCapture Render2dScript RayTraceMode XrayMode TwodPersistence; do
grep -q "$cap" "$ui/capabilities.rs" \
|| say "MISSING matrix entry: $cap (capabilities.rs)"
done
# 2. Default builds offer no reachable STEP action: the button
# carries the contained label, dispatch refuses via the matrix,
# and the only opt-in is the non-default cargo feature.
grep -q 'text: "STEP (off)"' "$ui/lib.rs" \
|| say "STEP button lost its contained label (lib.rs)"
if grep -n 'text: "STEP"' "$ui/lib.rs" | grep -v 'STEP (off)' | grep -v 'STEP (EXP' | grep -q .; then
say "bare STEP button label reintroduced (lib.rs)"
fi
grep -q 'step_export_enabled' "$ui/workspace.rs" \
|| say "export_step bypasses the capability matrix (workspace.rs)"
grep -q 'sync_capability_labels' "$ui/workspace_actions.rs" \
|| say "export dispatch lost its matrix label sync (workspace_actions.rs)"
grep -q 'experimental-step' crates/apps/cad/cad-ui/Cargo.toml \
|| say "experimental-step feature missing (cad-ui/Cargo.toml)"
if grep -rn 'experimental-step' "$ui" --include='*.rs' -l | grep -v -q -e 'capabilities.rs' -e 'workspace.rs'; then
say "experimental-step referenced outside capabilities.rs/workspace.rs"
fi
# 3. F12 writes no pixels: the synthetic gradient is gone from
# dispatch, and palette + keymap carry disabled copy.
if grep -q 'sky-to-ground' "$ui/workspace.rs"; then
say "synthetic F12 gradient reintroduced (workspace.rs)"
fi
if grep -q 'write_render_png' "$ui/workspace.rs"; then
say "F12 dispatch writes pixels again (workspace.rs)"
fi
grep -q 'Render High-Res Image (disabled' "$ui/command_palette.rs" \
|| say "palette lost its F12 disabled copy (command_palette.rs)"
grep -q 'F12.*disabled' "$ui/keymap.rs" \
|| say "keymap lost its F12 disabled copy (keymap.rs)"
# 4. render2d fails loudly: the binding records the call and eval
# converts it into a deterministic error (never silent 0.0).
grep -q 'RENDER2D_CALLED' "$ui/script_bindings.rs" \
|| say "render2d containment flag missing (script_bindings.rs)"
grep -q 'took_render2d_call' "$ui/script_bindings.rs" \
|| say "render2d eval error missing (script_bindings.rs)"
# 5. RayTrace slot is unreachable: dropdown index + palette cycle
# coerce through the matrix, and every label names the containment.
grep -q 'coerce_render_mode_index' "$ui/viewport.rs" \
|| say "dropdown coercion missing (viewport.rs)"
grep -q 'next_shading_index' "$ui/workspace.rs" \
|| say "palette cycle skips nothing (workspace.rs)"
grep -q 'Ray Trace (disabled)' "$ui/lib.rs" \
|| say "desktop render-mode label lost containment (lib.rs)"
grep -q 'Ray (off)' "$ui/lib.rs" \
|| say "mobile render-mode label lost containment (lib.rs)"
grep -q 'Ray (off)' "$ui/viewport_header.rs" \
|| say "header Ray label lost containment (viewport_header.rs)"
# 6. X-ray stays visibly experimental wherever it appears.
grep -q 'X-Ray (exp)' "$ui/lib.rs" \
|| say "X-Ray button lost its experimental label (lib.rs)"
if [ "$fail" -ne 0 ]; then
echo
echo "ERROR: UI-01 containment regressed (see lines above)."
echo "Restore the matrix-driven label/dispatch, never the old action."
exit 1
fi
echo "OK: UI-01 containment holds (STEP/F12/render2d/RayTrace/X-ray)"
# UI-02 session authority: identity, revision, and id supply are
# owned once by `cad-ui/src/session_controller.rs`, with the
# checked allocator in `scene_holder.rs`. Every sub-check below
# fails with the file that reintroduces a second authority, a
# public mutable parts vector, or an unchecked id path. Like the
# UI-00/UI-01 gates, these are static (no toolchain).
- name: UI-02 session authority stays singular
run: |
set -euo pipefail
ui=crates/apps/cad/cad-ui/src
fail=0
say() { echo "$1"; fail=1; }
# 1. The controller exists and names every authority symbol.
test -f "$ui/session_controller.rs" \
|| say "missing session controller (session_controller.rs)"
for sym in SessionId ProjectId DocumentId Revision DocumentDescriptor ControllerError CadSessionController try_reserve_up_to; do
grep -q "$sym" "$ui/session_controller.rs" \
|| say "MISSING authority symbol: $sym (session_controller.rs)"
done
grep -q 'pub mod session_controller' "$ui/lib.rs" \
|| say "controller not wired into the crate (lib.rs)"
# 2. The checked allocator exists next to the legacy one.
grep -q 'try_allocate' "$ui/scene_holder.rs" \
|| say "checked allocator missing (scene_holder.rs)"
grep -q 'enum AllocError' "$ui/scene_holder.rs" \
|| say "AllocError missing (scene_holder.rs)"
# 3. The dead parallel authority stays deleted.
if [ -f "$ui/document.rs" ]; then
say "dead document.rs authority reintroduced (delete it, route through the controller)"
fi
# 4. No public mutable parts vector: the canonical store keeps
# its nodes private and mutation goes through methods.
if grep -rn 'pub nodes' "$ui" --include='*.rs' | grep -q .; then
say "public mutable node vector reintroduced (keep nodes private)"
fi
# 5. The only whole-vec escape hatch stays test-confined.
if ! grep -B1 'fn as_mut_vec_without_bump' "$ui/scene_holder.rs" | grep -q 'cfg(test)'; then
say "as_mut_vec_without_bump lost its test-only confinement (scene_holder.rs)"
fi
if [ "$fail" -ne 0 ]; then
echo
echo "ERROR: UI-02 session authority regressed (see lines above)."
echo "Restore the single controller, never a second authority."
exit 1
fi
echo "OK: UI-02 session authority holds (identity/revision/checked ids)"
# UI-03a project repository: the filesystem side names no
# production path (every function takes an injected root), ids
# cross as validated slugs, writes are atomic, and opening
# returns an explicit outcome. Like the earlier gates, these are
# static (no toolchain).
- name: UI-03a project repository stays root-injected
run: |
set -euo pipefail
ui=crates/apps/cad/cad-ui/src
fail=0
say() { echo "$1"; fail=1; }
# 1. The repository exists and names every protocol symbol.
test -f "$ui/project_repo.rs" \
|| say "missing project repository (project_repo.rs)"
for sym in RepoRoot ProjectSlug ProjectManifest OpenOutcome RepoError SaveOptions FailPoint save_bytes_atomic SCHEMA_VERSION; do
grep -q "$sym" "$ui/project_repo.rs" \
|| say "MISSING repository symbol: $sym (project_repo.rs)"
done
grep -q 'pub mod project_repo' "$ui/lib.rs" \
|| say "repository not wired into the crate (lib.rs)"
# 2. No ambient production path: no store dir, no app-data
# dir, no thread-local active project, no store globals.
if grep -n 'store_dir\|app_data_dir\|ACTIVE_PROJECT\|get_active_project\|cad_projects_dir\|cad_store::\|project_store::' "$ui/project_repo.rs" | grep -q .; then
say "ambient production path in the repository (project_repo.rs must take only injected roots)"
fi
# 3. Slugs stay the only path identity: no raw-id path join.
if grep -n 'format!("{}' "$ui/project_repo.rs" | grep -v 'LEGACY_EXTENSION\|tmp-\|display()\|{reason}\|{e}\|{id\|{point\|{slug}\|{other\|{msg}\|{bad\|{ok}\|{stray' | grep -q .; then
say "unvalidated id reaches a path join (project_repo.rs)"
fi
if [ "$fail" -ne 0 ]; then
echo
echo "ERROR: UI-03a repository regressed (see lines above)."
echo "Restore injected roots and validated slugs, never ambient paths."
exit 1
fi
echo "OK: UI-03a repository holds (roots/slugs/atomic/outcomes)"
# CORE-01..12 canonical core: structured errors, budgets, checked
# ids, graph/transforms, versioned document, polygon pipeline, mesh
# hardening, atomic edits, world-mesh stream, bounded STL/DXF,
# quarantined STEP, exact URL policy. Static (no toolchain).
- name: CORE canonical modules stay complete
run: |
set -euo pipefail
core=crates/apps/cad/cad-core/src
fail=0
say() { echo "$1"; fail=1; }
for mod in error budgets checked_ids graph document polygon mesh_validate edit world_mesh url_policy; do
test -f "$core/$mod.rs" \
|| say "missing CORE module: $mod.rs"
grep -q "CORE-0\|CORE-1" "$core/$mod.rs" 2>/dev/null \
|| say "module $mod.rs lost its tranche header"
done
for sym in CadError ErrorKind ValidationPolicy GeometryBudget CheckedAllocator ExportIdAllocator RemapTable validate_graph world_matrix effective_visibility CadDocument EntityKind LengthUnit validate_polygon triangulate_profile validate_mesh checked_subdivide DocumentEdit ScenePatch stream_world_mesh classify_url; do
if ! grep -rq "$sym" "$core" --include='*.rs'; then
say "MISSING CORE symbol: $sym"
fi
done
grep -q 'pub mod error' "$core/lib.rs" || say "core modules not wired (lib.rs)"
# STEP quarantine: feature exists, default refuses, no hash dedup.
grep -q 'experimental-step' crates/apps/cad/cad-core/Cargo.toml \
|| say "experimental-step feature missing (cad-core/Cargo.toml)"
grep -q 'quarantined (experimental-step feature is off)' "$core/arch_step.rs" \
|| say "STEP default refusal missing (arch_step.rs)"
grep -q 'ExperimentalStepExporter' "$core/arch_step.rs" \
|| say "ExperimentalStep alias missing (arch_step.rs)"
if grep -q 'quantize(v: &Vec3d) -> u64' "$core/arch_step.rs"; then
say "hash vertex dedup reintroduced (arch_step.rs)"
fi
grep -q 'OPEN_SHELL' "$core/arch_step.rs" \
|| say "honest open-shell path missing (arch_step.rs)"
# URL trust: no prefix classification anywhere near a decision.
if grep -rn 'starts_with("http://127' "$core" --include='*.rs' | grep -q .; then
say "prefix URL classification reintroduced (use url_policy)"
fi
# CORE-04: no vertex-zero fan triangulator on runtime paths.
if grep -n 'tris.push(\[0, i as u32, (i + 1) as u32\])\|mesh_tris.push(\[0, i + 1, i\])' "$core/math.rs" "$core/cad_scene.rs" | grep -q .; then
say "fan triangulator reintroduced (use polygon::triangulate_indices / triangulate_profile)"
fi
grep -q 'crate::polygon::triangulate_indices' "$core/math.rs" \
|| say "math::triangulate_polygon no longer routes through the polygon pipeline"
grep -q 'fn try_extrude_polygon_mesh' "$core/cad_scene.rs" \
|| say "validated extrusion missing (cad_scene.rs)"
# Identity transform means identity (scale 1, not 0).
grep -q 'refusing to wrap\|scale: 1.0' "$core/cad_scene.rs" \
|| say "identity-transform contract weakened (cad_scene.rs)"
if [ "$fail" -ne 0 ]; then
echo
echo "ERROR: CORE canonical modules regressed (see lines above)."
exit 1
fi
echo "OK: CORE canonical modules hold (errors/budgets/ids/graph/doc/mesh/urls)"
# UI-03b..15 session modules: switch transactions, rebuild, journal,
# sandbox, AI correlation, bounded caches, valid BVH, one coordinate
# model, real capture, bounded exports, honest formats, lifecycle.
- name: UI session modules stay complete
run: |
set -euo pipefail
ui=crates/apps/cad/cad-ui/src
fail=0
say() { echo "$1"; fail=1; }
for mod in session_switch rebuild journal script_sandbox ai mesh_cache coords capture export_coordinator lifecycle; do
test -f "$ui/$mod.rs" \
|| say "missing UI module: $mod.rs"
done
for sym in SwitchableState switch_project RebuildCoordinator CommandJournal ScriptBudgets PreviewBuffer RevisionedCache PlaneBasis CaptureRequest ExportCoordinator LifecycleGate; do
if ! grep -rq "$sym" "$ui" --include='*.rs'; then
say "MISSING UI symbol: $sym"
fi
done
# BVH: permutation + validator, no direct-prims leaf walk.
grep -q 'order: Vec<u32>' "$ui/bvh.rs" \
|| say "BVH permutation missing (bvh.rs)"
grep -q 'pub fn validate' "$ui/bvh.rs" \
|| say "BVH structural validator missing (bvh.rs)"
if grep -q 'self.prims\[node.first' "$ui/bvh.rs"; then
say "direct-prims leaf walk reintroduced (bvh.rs must go through order)"
fi
# AI backend correctness: worker takes the selected backend and
# the local path fails closed without an endpoint.
grep -q 'fn new(_cx: &mut Cx, backend: BackendType)' "$ui/lib.rs" \
|| say "AI worker lost backend selection (lib.rs)"
grep -q 'local_openai_url().is_none()' "$ui/lib.rs" \
|| say "local fail-closed path missing (lib.rs)"
# Streaming preview must not write the editor (the apply path
# in apply_ai_response is the only editor writer for AI text).
if grep -n -A12 'fn stream_ai_response_to_editor' "$ui/workspace.rs" | grep -q 'set_editor_text_all'; then
say "destructive streaming reintroduced (workspace.rs preview must not write the editor)"
fi
# Export bound: dispatch refuses past the ceiling.
grep -q 'MAX_EXPORTS_IN_FLIGHT' "$ui/exporters.rs" \
|| say "export ceiling missing (exporters.rs)"
grep -q 'MAX_EXPORTS_IN_FLIGHT' "$ui/workspace.rs" \
|| say "export dispatch lost its bound (workspace.rs)"
# Script sandbox: source ceiling before the VM.
grep -q 'check_source' "$ui/script_bindings.rs" \
|| say "script source preflight missing (script_bindings.rs)"
if [ "$fail" -ne 0 ]; then
echo
echo "ERROR: UI session modules regressed (see lines above)."
exit 1
fi
echo "OK: UI session modules hold (switch/rebuild/journal/sandbox/ai/cache/bvh/coords/capture/exports/lifecycle)"
- name: Empty-target fixture proves gates fail
run: |
set -euo pipefail
empty=$(mktemp -d)
if find "$empty" -name '*.rs' | grep -q .; then
echo "ERROR: fresh temp dir is not empty -- fixture broken."
exit 1
fi
if [ "$(find "$empty" -name '*.rs' | wc -l)" -ne 0 ]; then
echo "ERROR: empty scan reported sources -- predicate broken."
exit 1
fi
echo "OK: empty fixture scans as empty (a gate over it would fail, not pass)"
rm -rf "$empty"
- name: Reject whitespace errors
run: git diff --check
# Exact package by Cargo package ID -- never a copied source harness.
cad-core-checks:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Install native dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq \
pkg-config libwayland-dev libxcursor-dev libxrandr-dev \
libxi-dev libx11-dev libgl1-mesa-dev libasound2-dev \
libglib2.0-dev libssl-dev libsqlite3-dev libudev-dev \
libpulse-dev libxkbcommon-dev
- name: Install the declared toolchain
run: |
set -e
version="$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
rust-toolchain.toml | head -n 1)"
curl --fail --location --proto '=https' --tlsv1.2 https://sh.rustup.rs -o /tmp/rustup-init
chmod 700 /tmp/rustup-init
/tmp/rustup-init -y --profile minimal --default-toolchain "$version" --no-modify-path
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Formatting (cad-core)
run: cargo fmt -p cad-core -- --check
- name: Check (cad-core)
run: cargo check --locked -p cad-core --all-targets
- name: Test (cad-core)
run: cargo test --locked -p cad-core --all-targets -- --test-threads=1
- name: Clippy (cad-core)
run: cargo clippy --locked -p cad-core --all-targets -- -D warnings
# Direct consumers of the public model. A red consumer here is a
# contract break or a known UI-00 baseline failure -- visible, not hidden.
#
# UI-00 lane split: pure library, integration, and real runtime UI
# results are recorded as SEPARATE artifacts so one lane cannot hide
# behind another's total. The lib lane was expected-red at the UI-00
# baseline (demo.rs wall-classification failure, since fixed by the
# UI-02 kind_hint fix); the integration lane runs the UI-15 matrix;
# the runtime lane proves the standalone binary compiles.
cad-consumers:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Install native dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq \
pkg-config libwayland-dev libxcursor-dev libxrandr-dev \
libxi-dev libx11-dev libgl1-mesa-dev libasound2-dev \
libglib2.0-dev libssl-dev libsqlite3-dev libudev-dev \
libpulse-dev libxkbcommon-dev
- name: Install the declared toolchain
run: |
set -e
version="$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
rust-toolchain.toml | head -n 1)"
curl --fail --location --proto '=https' --tlsv1.2 https://sh.rustup.rs -o /tmp/rustup-init
chmod 700 /tmp/rustup-init
/tmp/rustup-init -y --profile minimal --default-toolchain "$version" --no-modify-path
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Check (cad-ui)
run: cargo check --locked -p cad-ui --all-targets
# Lane 1: pure library unit tests (in-file #[cfg(test)]).
# Was expected-red at the UI-00 baseline (demo_has_slab_and_wall
# failed; fixed legitimately by the UI-02 kind_hint fix — see
# IGNORED_TESTS.md demo triage). The log is kept as its own
# artifact so any failure is inspectable, not a bare red step.
- name: Test (cad-ui lib lane)
run: |
set -euo pipefail
mkdir -p cad-artifacts
set +e
cargo test --locked -p cad-ui --lib -- --test-threads=1 2>&1 | tee cad-artifacts/cad-ui-lib.log
status=${PIPESTATUS[0]}
set -e
echo "lib lane exit: $status" | tee -a cad-artifacts/cad-ui-lib.log
exit "$status"
# Lane 2: integration tests (tests/ targets). UI-15 owns the
# runtime/migration matrix (project_lifecycle, script_limits,
# bvh_differential, export_interop, runtime_ui); CORE-12 owns
# cad-core's (resource_limits, format_interop, migration_corpus).
# An empty lane is recorded as empty, never as green coverage.
- name: Test (cad-ui integration lane)
run: |
set -euo pipefail
mkdir -p cad-artifacts
if ls crates/apps/cad/cad-ui/tests/*.rs >/dev/null 2>&1; then
cargo test --locked -p cad-ui --tests -- --test-threads=1 2>&1 | tee cad-artifacts/cad-ui-integration.log
else
echo "cad-ui integration lane: no tests/*.rs targets exist yet (UI-15 owns runtime/migration matrix)." | tee cad-artifacts/cad-ui-integration.log
echo "This empty lane is recorded, not counted as coverage." | tee -a cad-artifacts/cad-ui-integration.log
fi
# Lane 3: real runtime UI — the standalone binary must compile.
# Headless CI cannot run the Makepad event loop; this lane proves
# the binary target builds and records which binary was built.
# Runtime behavior matrix itself is owned by UI-15.
- name: Build (cad-ui runtime lane)
run: |
set -euo pipefail
mkdir -p cad-artifacts
cargo check --locked -p cad-ui --bins 2>&1 | tee cad-artifacts/cad-ui-runtime.log
echo "--- bins ---" | tee -a cad-artifacts/cad-ui-runtime.log
ls crates/apps/cad/cad-ui/src/bin/ | tee -a cad-artifacts/cad-ui-runtime.log
- name: Upload cad-ui lane artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: cad-ui-lanes
path: cad-artifacts/
if-no-files-found: error
- name: Check (nigig-build)
run: cargo check --locked -p nigig-build --all-targets