forked from EduCraft/curriculum-project-hub
chore(spec+checker): cleanup pass — trim docs, reorg Courseware, covers-as-data, fold DanglingReference (ADR-0012)
Spec母本噪音清理 + 一处 spec↔impl 对齐 + 一处契约自洽修正。 Part A — spec doc 瘦身:每条 doc 收到"语义点 + 标签 + ADR ref + 承载性 why", 把跨文件复读的方法论(element-kind 开放性对比、likec4 画不出、分歧点测试)上移到 module header。OPEN 框架保持清晰(RunState/Capability 完整性仍明示须 surface)。 Part B — Courseware/ 由 10 文件平铺重组为 Model/ Export/ Check/ Open/ 四子命名空间 (namespace Spec.Courseware 不变,零引用改动)+ 四个子 aggregator。lake build 绿(24 jobs)。 Part C — 渲染覆盖落为数据(ADR-0011 对齐):去掉 cph-check 里硬编码的 COVERED_TARGETS,改读 TargetConfig.covers。cph-model 新增 covers: Option<Vec<String>> (None=未声明,由 cph-check 默认为全部 known kinds;显式 [] 表示不覆盖任何 kind)。 新增 3 个覆盖行为测试。 ADR-0012 — DanglingReference 退役(诊断 7→6 类):其两种情形(未解析 @ref、越界/缺失 相对 import)都是 typst 编译期失败,归 typstCompile。同步移除 Oracle.refsResolve (被 compiles 蕴含),Legal 少一个合取项。impl 删去从未被发射的 DiagCode::DanglingReference, 闭合 named-but-unemitted 的 spec↔impl 缝。ADR-0010 加修订指针。 OPEN 点(RunState/Capability 完整性、QuestionBank、Course)按既定纪律保持 OPEN,本次 只改善其框架措辞,不决策。 验证:spec lake build 绿;cargo test 全绿(含新增覆盖测试);clippy 静默; TH-141 check 0 errors/0 warnings 无回归。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
//! `<root>/manifest.toml` (project / info / ordered `[[parts]]` / declared
|
||||
//! `[targets.*]`) and each part's `<root>/<path>/element.toml`, and produces an
|
||||
//! ordered [`Lesson`] — mirroring the Lean master's `Lesson = List (Element P)`
|
||||
//! (`spec/Spec/Courseware/Lesson.lean`), where the order of `parts` carries
|
||||
//! (`spec/Spec/Courseware/Model/Lesson.lean`), where the order of `parts` carries
|
||||
//! teaching semantics.
|
||||
//!
|
||||
//! Scope boundaries (deliberately staying in lane):
|
||||
@@ -72,6 +72,9 @@ impl Lesson {
|
||||
/// - A target with **no `steps`** defaults to a single
|
||||
/// [`Step::TypstCompile`] with `template = "exports/<name>.typ"` (the
|
||||
/// framework's stock per-target template).
|
||||
/// - A target with **no `covers`** key leaves [`TargetConfig::covers`] `None`,
|
||||
/// meaning "coverage not declared". The consumer (`cph-check`) resolves that
|
||||
/// to the full known-kind universe — see the field doc.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct TargetConfig {
|
||||
/// Target name, e.g. `"student"` (the `[targets.<name>]` table key).
|
||||
@@ -82,12 +85,25 @@ pub struct TargetConfig {
|
||||
/// The ordered build steps. Defaults to a single [`Step::TypstCompile`]
|
||||
/// with template `exports/<name>.typ` when no `[[steps]]` are given.
|
||||
pub steps: Vec<Step>,
|
||||
/// The **render-coverage declaration**: which element kinds this target
|
||||
/// renders. Realizes `Spec.Courseware.TargetSpec.covers : KindId → Prop`
|
||||
/// (`spec/Spec/Courseware/Export/Render.lean`) and ADR-0011's "render
|
||||
/// coverage is a declaration, not a payload": the contract keeps *which
|
||||
/// kinds a target renders* (used by the `renderIgnored` seed diagnostic),
|
||||
/// while the rendering "how" lives in the template/steps.
|
||||
///
|
||||
/// `Some(kinds)` is the explicit manifest list (`covers = ["segment", …]`).
|
||||
/// `None` means the `covers` key was **absent**; the loader does not own the
|
||||
/// kind universe (it must not depend on `cph-schema`), so it records the
|
||||
/// absence and lets `cph-check` default it to all known kinds. An empty
|
||||
/// `Some(vec![])` is distinct: a target that explicitly covers nothing.
|
||||
pub covers: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// The artifact an export target produces (ADR-0009/0011).
|
||||
///
|
||||
/// **Mirrors `Spec.Courseware.Artifact`** in the Lean semantic master
|
||||
/// (`spec/Spec/Courseware/Artifact.lean`), whose definition is exactly:
|
||||
/// (`spec/Spec/Courseware/Export/Artifact.lean`), whose definition is exactly:
|
||||
///
|
||||
/// ```text
|
||||
/// inductive Artifact where
|
||||
@@ -141,7 +157,7 @@ impl Artifact {
|
||||
/// One typed build step (ADR-0011).
|
||||
///
|
||||
/// **Mirrors `Spec.Courseware.Step`** in the Lean semantic master
|
||||
/// (`spec/Spec/Courseware/Render.lean`), whose definition is exactly:
|
||||
/// (`spec/Spec/Courseware/Export/Render.lean`), whose definition is exactly:
|
||||
///
|
||||
/// ```text
|
||||
/// inductive Step where
|
||||
@@ -572,6 +588,7 @@ fn parse_target(name: String, value: toml::Value, diags: &mut Vec<Diagnostic>) -
|
||||
return TargetConfig {
|
||||
artifact: Artifact::default_for(&name),
|
||||
steps: vec![Step::default_for(&name)],
|
||||
covers: None,
|
||||
name,
|
||||
};
|
||||
}
|
||||
@@ -587,10 +604,78 @@ fn parse_target(name: String, value: toml::Value, diags: &mut Vec<Diagnostic>) -
|
||||
Some(value) => parse_steps(&name, value, diags),
|
||||
};
|
||||
|
||||
let covers = match table.remove("covers") {
|
||||
None => None,
|
||||
Some(value) => parse_covers(&name, value, diags),
|
||||
};
|
||||
|
||||
TargetConfig {
|
||||
name,
|
||||
artifact,
|
||||
steps,
|
||||
covers,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a target's optional `covers` array into the render-coverage declaration
|
||||
/// (ADR-0011). Each entry is a kind name (a string). A non-array `covers`, or any
|
||||
/// non-string entry, is reported as a [`DiagCode::SchemaViolation`]; in that case
|
||||
/// the declaration falls back to `None` ("not declared"), so `cph-check` defaults
|
||||
/// it to the full known-kind universe rather than silently covering nothing.
|
||||
///
|
||||
/// A well-formed but empty `covers = []` yields `Some(vec![])`: a target that
|
||||
/// explicitly renders no kind (every used kind then draws a `renderIgnored`
|
||||
/// warning) — distinct from an absent key.
|
||||
fn parse_covers(name: &str, value: toml::Value, diags: &mut Vec<Diagnostic>) -> Option<Vec<String>> {
|
||||
let items = match value {
|
||||
toml::Value::Array(items) => items,
|
||||
_ => {
|
||||
diags.push(
|
||||
Diagnostic::error(
|
||||
DiagCode::SchemaViolation,
|
||||
format!(
|
||||
"target '{name}' has a non-array `covers`; declare it as a list of \
|
||||
kind names, e.g. `covers = [\"segment\", \"example\"]`"
|
||||
),
|
||||
)
|
||||
.with_hint("set `covers` to an array of kind-name strings, or omit it"),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut kinds = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
match item {
|
||||
toml::Value::String(s) => kinds.push(s),
|
||||
other => {
|
||||
diags.push(
|
||||
Diagnostic::error(
|
||||
DiagCode::SchemaViolation,
|
||||
format!(
|
||||
"target '{name}' has a non-string entry in `covers`: {}",
|
||||
json_like_type(&other)
|
||||
),
|
||||
)
|
||||
.with_hint("each `covers` entry is a kind name (a string)"),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(kinds)
|
||||
}
|
||||
|
||||
/// A short type label for a TOML value, for the `covers` non-string diagnostic.
|
||||
fn json_like_type(v: &toml::Value) -> &'static str {
|
||||
match v {
|
||||
toml::Value::String(_) => "string",
|
||||
toml::Value::Integer(_) => "integer",
|
||||
toml::Value::Float(_) => "float",
|
||||
toml::Value::Boolean(_) => "boolean",
|
||||
toml::Value::Datetime(_) => "datetime",
|
||||
toml::Value::Array(_) => "array",
|
||||
toml::Value::Table(_) => "table",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user