forked from bai/curriculum-project-hub
feat(model+spec): .cph-version 契约文件 + CphVersionMismatch 诊断(ADR-0016);bump 0.0.2
教研工程文件根放 .cph-version(内容=面向的 cph 版本)。cph 加载时比对自身版本
(CARGO_PKG_VERSION),不相容报 CphVersionMismatch error 并拒绝。当前判定为版本完全
相等(MVP);判定逻辑孤立在 versions_compatible 单谓词,后续可放宽为 semver 区间而
不动诊断分类/spec/CLI。
spec(7 类诊断,原 6 类):
- Diagnostic.lean: DiagKind 增 cphVersionMismatch(error 级),分类注释 6→7
- Pipeline.lean: load 阶段含 .cph-version 兼容性判定
- Courseware.lean: ADR 区间 →0016
实现:
- cph-diag: DiagCode::CphVersionMismatch("E-CPH-VERSION")
- cph-model: load() 解析 manifest 成功后 check_cph_version;versions_compatible
谓词(完全相等);CPH_VERSION const。missing .cph-version 暂跳过(迁移期 OPEN);
空文件报 error
- examples/TH-141、valid/mini fixture、KenKen 课各加 .cph-version=0.0.2
测试:cph-model 4 个版本门测试;全 workspace 53 passed;lake 25 jobs 绿。
负向验证:9.9.9 .cph-version → E-CPH-VERSION error + check 拒绝(exit 1)。
bump: workspace.package 0.0.1→0.0.2;cph --version 报 cph 0.0.2;README 同步
(含版本契约说明)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -391,6 +391,14 @@ pub fn load(root: &Path) -> (Option<Lesson>, Vec<Diagnostic>) {
|
||||
}
|
||||
};
|
||||
|
||||
// `.cph-version` compatibility (ADR-0016). Decided at load time, before
|
||||
// any structural/schema/compile work. An incompatible version is an
|
||||
// error diagnostic (`CphVersionMismatch`); it does not halt loading, so
|
||||
// any other defects surface alongside it — but an error diagnostic alone
|
||||
// already makes the lesson illegal (ADR-0010), so `check`/`build` refuse.
|
||||
// A missing `.cph-version` is skipped for now (ADR-0016 OPEN).
|
||||
check_cph_version(root, &mut diags);
|
||||
|
||||
// [project] and [info] are required to build a Lesson at all.
|
||||
let project = match raw.project {
|
||||
Some(p) => Project {
|
||||
@@ -506,6 +514,58 @@ pub fn load(root: &Path) -> (Option<Lesson>, Vec<Diagnostic>) {
|
||||
(Some(lesson), diags)
|
||||
}
|
||||
|
||||
/// The cph version the running CLI was built with (ADR-0016). Pulled from the
|
||||
/// crate's `CARGO_PKG_VERSION` at compile time.
|
||||
pub const CPH_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Read the engineering file's `.cph-version` and, if present, push a
|
||||
/// `CphVersionMismatch` error when it is not compatible with [`CPH_VERSION`]
|
||||
/// (ADR-0016). A missing file is skipped for now (migration period; OPEN in
|
||||
/// ADR-0016).
|
||||
fn check_cph_version(root: &Path, diags: &mut Vec<Diagnostic>) {
|
||||
let path = root.join(".cph-version");
|
||||
let Ok(src) = std::fs::read_to_string(&path) else {
|
||||
return; // missing `.cph-version` — skipped (ADR-0016 OPEN).
|
||||
};
|
||||
let file_version = src.trim();
|
||||
if file_version.is_empty() {
|
||||
diags.push(
|
||||
Diagnostic::error(
|
||||
DiagCode::CphVersionMismatch,
|
||||
format!(".cph-version is empty; expected cph version {CPH_VERSION}"),
|
||||
)
|
||||
.with_hint(format!(
|
||||
"write the cph version this file targets into {} (currently {CPH_VERSION})",
|
||||
path.display()
|
||||
)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if !versions_compatible(file_version, CPH_VERSION) {
|
||||
diags.push(
|
||||
Diagnostic::error(
|
||||
DiagCode::CphVersionMismatch,
|
||||
format!(
|
||||
".cph-version declares {file_version}, but this cph is {CPH_VERSION} (incompatible)"
|
||||
),
|
||||
)
|
||||
.with_hint(format!(
|
||||
"align them: set .cph-version to {CPH_VERSION}, or install cph {file_version}"
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an engineering file's declared cph version is compatible with the
|
||||
/// running CLI's version (ADR-0016). **The rule is exact equality** — the
|
||||
/// strictest choice, deliberately, while the format is young (0.0.x). This is
|
||||
/// the single seam for future relaxation to a semver range: relaxing the
|
||||
/// format does not change the diagnostic class, the spec, or the CLI — only
|
||||
/// this predicate.
|
||||
fn versions_compatible(file_version: &str, cli_version: &str) -> bool {
|
||||
file_version == cli_version
|
||||
}
|
||||
|
||||
/// Load one element's `element.toml` into an [`ElementDescriptor`], collecting
|
||||
/// diagnostics. On a missing/malformed `element.toml` the descriptor falls back
|
||||
/// to the part's declared kind with empty scalars so the lesson stays buildable.
|
||||
|
||||
Reference in New Issue
Block a user