chore: scaffold nine-crate cargo workspace with headless selftest gate

Empty-but-compiling crates enforce the architecture from day one: forbid(unsafe_code) everywhere except vernier-occt-sys, workspace lints deny missing docs and unwrap/expect, and the downward-only dependency rule is now compiler-checked. vernier-cli --selftest runs headless and its output is asserted byte-identical across runs (invariant #1). system-occt is a declared no-op feature so the documented fast-iteration commands work before the Phase 1 bridge lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 23:32:17 +02:00
co-authored by Claude Fable 5
parent 9a83c7feee
commit 847d5ea1a7
23 changed files with 315 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
Generated
+39
View File
@@ -0,0 +1,39 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "vernier-app"
version = "0.0.1"
[[package]]
name = "vernier-cli"
version = "0.0.1"
[[package]]
name = "vernier-doc"
version = "0.0.1"
[[package]]
name = "vernier-kernel"
version = "0.0.1"
[[package]]
name = "vernier-occt-sys"
version = "0.0.1"
[[package]]
name = "vernier-render"
version = "0.0.1"
[[package]]
name = "vernier-solver"
version = "0.0.1"
[[package]]
name = "vernier-tess"
version = "0.0.1"
[[package]]
name = "vernier-ui"
version = "0.0.1"
+25
View File
@@ -0,0 +1,25 @@
[workspace]
resolver = "3"
members = ["crates/*"]
[workspace.package]
version = "0.0.1"
edition = "2024"
rust-version = "1.96"
license = "GPL-3.0-or-later"
repository = "https://git.briggen.dev/NilsBriggen/vernier"
publish = false
[workspace.dependencies]
indexmap = "2"
thiserror = "2"
tracing = "0.1"
# Deny-by-default discipline from CLAUDE.md. Test code may locally
# `#[allow(clippy::unwrap_used)]` — production code may not.
[workspace.lints.rust]
missing_docs = "deny"
[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "vernier-app"
description = "VernierCAD desktop application"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[features]
# Forward to vernier-occt-sys/system-occt once the kernel dependency chain
# reaches this crate (Phase 1). Declared now so
# `cargo run -p vernier-app --features system-occt` works as documented.
system-occt = []
[lints]
workspace = true
+9
View File
@@ -0,0 +1,9 @@
//! Thin `main()` for the VernierCAD desktop application.
//!
//! Everything of substance lives in the library crates; this binary only
//! wires them together. The egui/wgpu shell arrives with the Phase 0 render
//! milestone.
#![forbid(unsafe_code)]
fn main() {}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "vernier-cli"
description = "Headless driver for scripting and testing"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lints]
workspace = true
+32
View File
@@ -0,0 +1,32 @@
//! Headless driver for VernierCAD — the primary verification path.
//!
//! Every modelling operation must be drivable through this binary with no
//! window, no GPU, and no display, producing a deterministic artifact that
//! can be asserted on. A feature that cannot be exercised here is not
//! finished.
#![forbid(unsafe_code)]
use std::process::ExitCode;
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
Some("--selftest") => selftest(),
_ => {
eprintln!("usage: vernier-cli --selftest");
ExitCode::FAILURE
}
}
}
/// Runs every registered self-check headlessly and prints a JSON report to
/// stdout. The report must be byte-identical across runs (invariant #1).
///
/// Phase 0 gate: passes with no display attached. Subsystem checks register
/// here as they land; an empty run still proves the binary works headless.
fn selftest() -> ExitCode {
let report = r#"{"selftest":"pass","checks":[]}"#;
println!("{report}");
ExitCode::SUCCESS
}
+32
View File
@@ -0,0 +1,32 @@
//! The Phase 0 gate: `--selftest` passes with no display, deterministically.
#![allow(clippy::unwrap_used)]
use std::process::{Command, Output};
fn run_selftest() -> Output {
Command::new(env!("CARGO_BIN_EXE_vernier-cli"))
.arg("--selftest")
.env_remove("DISPLAY")
.env_remove("WAYLAND_DISPLAY")
.output()
.unwrap()
}
#[test]
fn selftest_passes_headless() {
let out = run_selftest();
assert!(out.status.success(), "selftest exited non-zero");
let report = String::from_utf8(out.stdout).unwrap();
assert!(report.contains(r#""selftest":"pass""#), "report: {report}");
}
#[test]
fn selftest_output_is_deterministic() {
let first = run_selftest();
let second = run_selftest();
assert_eq!(
first.stdout, second.stdout,
"selftest output differs between runs — invariant #1 violated"
);
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "vernier-doc"
description = "Document model: feature DAG, EntityId space, undo stack, serialization"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lints]
workspace = true
+7
View File
@@ -0,0 +1,7 @@
//! Document model: feature DAG, `EntityId` allocation, undo stack, serialization.
//!
//! `EntityId`s are allocated here and mapped through the kernel façade —
//! entity identity never derives from an index or a pointer. Like
//! `vernier-solver`, this crate must never depend on `vernier-kernel`.
#![forbid(unsafe_code)]
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "vernier-kernel"
description = "Kernel abstraction: trait Kernel, OpResult, EntityId"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lints]
workspace = true
+8
View File
@@ -0,0 +1,8 @@
//! Kernel abstraction: `trait Kernel`, `OpResult`, `EntityId`.
//!
//! Every kernel operation returns a shape *plus a history delta* describing
//! which entities were generated, modified, or deleted — an operation that
//! returns a bare shape is incomplete. The OCCT implementation lives behind
//! this boundary so hot paths can be replaced later without touching callers.
#![forbid(unsafe_code)]
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "vernier-occt-sys"
description = "cxx bridge and C++ facade to the OCCT geometry kernel"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[features]
# Build against the distro OCCT package instead of the vendored submodule.
# No-op until the Phase 1 bridge lands; declared now so the fast-iteration
# commands documented in CLAUDE.md work from day one.
system-occt = []
[lints]
workspace = true
+9
View File
@@ -0,0 +1,9 @@
//! FFI bridge to OCCT via a thin C++ façade with opaque `u64` shape handles.
//!
//! This is the **only** crate in the workspace permitted `unsafe`. The façade
//! owns a shape registry on the C++ side; OCCT's `Handle<T>` never crosses the
//! FFI boundary, and every OCCT exception is caught in C++ and converted to a
//! result code before it can unwind into Rust.
//!
//! The bridge itself arrives in Phase 1. `facade.hpp` will be the complete API
//! surface.
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "vernier-render"
description = "wgpu renderer with ID-buffer picking"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lints]
workspace = true
+8
View File
@@ -0,0 +1,8 @@
//! wgpu renderer: viewport, camera, ID-buffer picking.
//!
//! The render thread is sacred — it never blocks on the kernel. Kernel work
//! runs on a worker; the viewport orbits at refresh rate during an
//! eight-second recompute or the design is wrong. Target GPU is AMD RDNA3 via
//! Mesa/RADV (Vulkan); never assume NVIDIA.
#![forbid(unsafe_code)]
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "vernier-solver"
description = "2D sketch constraint solver (sparse Jacobian, Levenberg-Marquardt)"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lints]
workspace = true
+8
View File
@@ -0,0 +1,8 @@
//! 2D sketch constraint solver: sparse Jacobian + Levenberg–Marquardt.
//!
//! Depends on **nothing** from the kernel layer, by design — that is what
//! keeps it fast to test and small enough to reason about in one sitting.
//! If a task here seems to need `vernier-kernel`, the abstraction is wrong;
//! raise it rather than adding the dependency.
#![forbid(unsafe_code)]
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "vernier-tess"
description = "Tessellation and tessellation cache"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lints]
workspace = true
+3
View File
@@ -0,0 +1,3 @@
//! Tessellation of kernel shapes into render meshes, plus the tessellation cache.
#![forbid(unsafe_code)]
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "vernier-ui"
description = "egui shell: viewport widget, timeline"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lints]
workspace = true
+7
View File
@@ -0,0 +1,7 @@
//! egui application shell: viewport widget, timeline, panels.
//!
//! egui sits behind an abstraction so it can be swapped later without
//! touching the rest of the stack. Degrees exist only at this boundary —
//! everything below works in radians and millimetres.
#![forbid(unsafe_code)]
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env fish
# Full check — must pass before any commit.
cargo fmt --check
and cargo clippy --all-targets -- -D warnings
and cargo test --workspace
and cargo run -q -p vernier-cli -- --selftest