forked from EduCraft/curriculum-project-hub
feat(checker): cph-check orchestrator + cph-cli cph binary (WU-5)
Closes the core pipeline: the `cph` CLI checks an engineering file and compiles a PDF. cph-check — phases (a) load → (b) structural (known-kind set) → (c) schema → (d) typst compile → (e) render-coverage. Compile is gated: skipped if (a)-(c) produced any Error (broken input would only add noise); `check` compiles every declared target (dedup identical diags), `build` compiles the one requested. Render-coverage emits a non-blocking RenderIgnored *warning* when a (kind, target) has no render rule — severity pinned via RENDER_IGNORED_SEVERITY const citing spec Diagnostic.renderIgnoredSeverity + ADR-0005. No double-emit: unknown/missing parts are skipped downstream. API: check(root, engine) -> CheckReport, build(root, engine, target) -> (Option<pdf>, CheckReport). cph-cli — clap: `cph check <path>` (exit 1 on any Error, warnings → 0) and `cph build <path> --target <name> [-o out]` (default target student, default out <path>/build/<target>.pdf). Diagnostics → stderr via Display, results → stdout. --render-dir override. End-to-end on examples/TH-141 (39 parts): `check` → 0 errors/0 warnings; `build` → real PDF student 9pp/395KB, teacher 12pp/474KB. Broken-element check (unclosed delimiter in a stmt.typ) → span-accurate `error[E-TYPST-COMPILE]: unclosed delimiter --> lemmas/…/stmt.typ:7:8`, exit 1. Workspace: fmt + clippy -D warnings + test all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,3 +9,7 @@ name = "cph"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
cph-check = { path = "../cph-check" }
|
||||
cph-diag = { workspace = true }
|
||||
cph-typst = { path = "../cph-typst" }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
+129
-6
@@ -1,10 +1,133 @@
|
||||
//! `cph` — the command-line entrypoint for the checker.
|
||||
//!
|
||||
//! Owned by **WU-5**. Will parse args, run the `cph-check` pipeline against an
|
||||
//! engineering file, and print diagnostics. A stub for now so the workspace
|
||||
//! builds and produces a runnable binary.
|
||||
//! Owned by **WU-5**. Parses args, constructs the typst [`Engine`], runs the
|
||||
//! `cph-check` pipeline against an engineering file, and prints diagnostics.
|
||||
//!
|
||||
//! Convention: **diagnostics go to stderr, results go to stdout.** `check`
|
||||
//! exits 1 when there is any `Error`-severity diagnostic (warnings alone exit
|
||||
//! 0); `build` exits 1 when the PDF could not be produced.
|
||||
|
||||
// WU-5: wire up arg parsing + the check pipeline + diagnostic rendering.
|
||||
fn main() {
|
||||
println!("cph: not yet implemented");
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use cph_check::CheckReport;
|
||||
use cph_typst::Engine;
|
||||
|
||||
/// The `cph` checker for curriculum engineering files.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "cph", version, about, long_about = None)]
|
||||
struct Cli {
|
||||
/// Override the `cph-render` package directory (the folder containing
|
||||
/// `lib.typ` / `typst.toml`). Defaults to the engine's own resolution
|
||||
/// (`CPH_RENDER_DIR` env var, else the repo `render/`).
|
||||
#[arg(long, global = true, value_name = "DIR")]
|
||||
render_dir: Option<PathBuf>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
/// Run the full check pipeline and print diagnostics. Exits 1 on any error.
|
||||
Check {
|
||||
/// Path to the engineering-file root (the folder with `manifest.toml`).
|
||||
path: PathBuf,
|
||||
},
|
||||
/// Build a PDF for a render target. Exits 1 if the build fails.
|
||||
Build {
|
||||
/// Path to the engineering-file root (the folder with `manifest.toml`).
|
||||
path: PathBuf,
|
||||
/// Render target to export.
|
||||
#[arg(long, default_value = "student")]
|
||||
target: String,
|
||||
/// Output PDF path. Defaults to `<PATH>/build/<target>.pdf`.
|
||||
#[arg(short = 'o', long, value_name = "OUT")]
|
||||
out: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let engine = match &cli.render_dir {
|
||||
Some(dir) => Engine::with_render_dir(dir.clone()),
|
||||
None => Engine::new(),
|
||||
};
|
||||
|
||||
match cli.command {
|
||||
Command::Check { path } => run_check(&path, &engine),
|
||||
Command::Build { path, target, out } => run_build(&path, &engine, &target, out),
|
||||
}
|
||||
}
|
||||
|
||||
/// Print every diagnostic in `report` to stderr, followed by a summary line.
|
||||
fn print_diagnostics(report: &CheckReport) {
|
||||
for d in &report.diagnostics {
|
||||
eprintln!("{d}");
|
||||
}
|
||||
eprintln!(
|
||||
"{} error{}, {} warning{}",
|
||||
report.error_count(),
|
||||
plural(report.error_count()),
|
||||
report.warning_count(),
|
||||
plural(report.warning_count()),
|
||||
);
|
||||
}
|
||||
|
||||
fn plural(n: usize) -> &'static str {
|
||||
if n == 1 {
|
||||
""
|
||||
} else {
|
||||
"s"
|
||||
}
|
||||
}
|
||||
|
||||
fn run_check(path: &std::path::Path, engine: &Engine) -> ExitCode {
|
||||
let report = cph_check::check(path, engine);
|
||||
print_diagnostics(&report);
|
||||
|
||||
if report.has_errors() {
|
||||
ExitCode::FAILURE
|
||||
} else {
|
||||
println!("check passed: {}", path.display());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
fn run_build(
|
||||
path: &std::path::Path,
|
||||
engine: &Engine,
|
||||
target: &str,
|
||||
out: Option<PathBuf>,
|
||||
) -> ExitCode {
|
||||
let out_path = out.unwrap_or_else(|| path.join("build").join(format!("{target}.pdf")));
|
||||
|
||||
let (pdf, report) = cph_check::build(path, engine, target);
|
||||
print_diagnostics(&report);
|
||||
|
||||
match pdf {
|
||||
Some(bytes) => {
|
||||
if let Some(parent) = out_path.parent() {
|
||||
if let Err(e) = std::fs::create_dir_all(parent) {
|
||||
eprintln!(
|
||||
"error: cannot create output directory '{}': {e}",
|
||||
parent.display()
|
||||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
if let Err(e) = std::fs::write(&out_path, &bytes) {
|
||||
eprintln!("error: cannot write '{}': {e}", out_path.display());
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
println!("wrote {} ({} bytes)", out_path.display(), bytes.len());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
None => {
|
||||
eprintln!("build failed: {} errors", report.error_count());
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user