- 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
603 lines
30 KiB
YAML
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
|