feat(spec+checker): slides/逐字稿 markdown 内容面 + assembleMarkdown step(ADR-0015)

给课程加两个 markdown 内容面:slides(大纲:h2 part → h3 小节 → ![]() 图 +
:::widget 教具)与逐字稿(口播)。直接 markdown+KaTeX 撰写,绕开 typst→md
公式转换(等于在 slides 面选 ADR-0014 的 R2)。

spec:
- RichContent.lean: content leaf 带 format(typst|markdown),新增 ContentFormat
  归纳,RichContentRef 加 format 字段
- Export/Render.lean: Step 加 assembleMarkdown (field),钉执行语义
  (同 shell 三边界 + 注入课程 h1 + 自包含收集引用图)
- Courseware.lean: ADR 区间 →0015

实现:
- cph-schema: x-cph-content 支持 true→.typ / 字符串→该扩展名(.md);
  ContentField 带 ext;4 个 kind schema 各加可选 slides/transcript
- cph-model: Step::AssembleMarkdown{field} + manifest "assemble-markdown" 解析
- cph-check: run_markdown_assemble_target / target_is_markdown_assemble
  (照 run_shell_target 形状:check 门控、注入 h1、按 [[parts]] 序拼、写盘后
  收集 ![](rel) 引用图进 build 根使产物自包含、引用图缺失属 build-过程错误
  不入 6 类诊断);compile_targets 已 filter TypstCompile,纯 assemble 自然排除
- cph-cli: run_markdown_assemble_build 路由
- cph-typst: template_step 补 AssembleMarkdown => None 分支

测试:全 workspace 49 passed(新增 6 个 markdown 装配测试含图收集);
lake build 25 jobs 绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 14:55:55 +08:00
parent 9590e15236
commit 144e2d8c80
14 changed files with 1053 additions and 42 deletions
+42 -7
View File
@@ -163,6 +163,7 @@ impl Artifact {
/// inductive Step where
/// | typstCompile (template : String)
/// | shell (run : String)
/// | assembleMarkdown (field : String)
/// ```
///
/// A step is a *typed* operation (extensible): `TypstCompile` compiles a
@@ -170,9 +171,13 @@ impl Artifact {
/// the manifest into it (`--input manifest=…`), which a bare `typst compile`
/// string cannot express. `Shell` is the escape hatch for steps that resist
/// declaration (ADR-0005's medium-only category (b): HTML interactive / npm
/// builds). The on-disk `[[steps]]` entry's `type` discriminator selects the
/// variant: `"typst-compile"` → [`Step::TypstCompile`], `"shell"` →
/// [`Step::Shell`].
/// builds). `AssembleMarkdown` concatenates a per-element markdown content
/// field into a single-file markdown deliverable (ADR-0015: the slides outline
/// and 逐字稿 transcript surfaces, authored directly in markdown+KaTeX to
/// sidestep the typst→md conversion). The on-disk `[[steps]]` entry's `type`
/// discriminator selects the variant: `"typst-compile"` →
/// [`Step::TypstCompile`], `"shell"` → [`Step::Shell`], `"assemble-markdown"`
/// → [`Step::AssembleMarkdown`].
///
/// As with [`Artifact`], this alignment is maintained by review, not CI.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
@@ -189,6 +194,15 @@ pub enum Step {
/// The command line to run.
run: String,
},
/// Assemble a single-file markdown deliverable by concatenating each
/// element's `field` markdown content file in `[[parts]]` order. Mirrors
/// Lean `Step.assembleMarkdown` (ADR-0015). Not a typst build — the
/// framework owns the read/concatenate/write itself.
AssembleMarkdown {
/// The per-element markdown content field to assemble (e.g. `slides`,
/// `transcript`).
field: String,
},
}
impl Step {
@@ -901,7 +915,9 @@ fn parse_step(name: &str, value: toml::Value, diags: &mut Vec<Diagnostic>) -> Op
DiagCode::SchemaViolation,
format!("target '{name}' has a step with a non-string `type`"),
)
.with_hint("set the step `type` to \"typst-compile\" or \"shell\""),
.with_hint(
"set the step `type` to \"typst-compile\", \"shell\", or \"assemble-markdown\"",
),
);
return None;
}
@@ -911,7 +927,9 @@ fn parse_step(name: &str, value: toml::Value, diags: &mut Vec<Diagnostic>) -> Op
DiagCode::SchemaViolation,
format!("target '{name}' has a step with no `type` discriminator"),
)
.with_hint("add `type = \"typst-compile\"` (or `\"shell\"`) to the step"),
.with_hint(
"add `type = \"typst-compile\"` (or `\"shell\"`, `\"assemble-markdown\"`) to the step",
),
);
return None;
}
@@ -948,16 +966,33 @@ fn parse_step(name: &str, value: toml::Value, diags: &mut Vec<Diagnostic>) -> Op
None
}
},
"assemble-markdown" => match take_string_field(&mut table, "field") {
Some(field) => Some(Step::AssembleMarkdown { field }),
None => {
diags.push(
Diagnostic::error(
DiagCode::SchemaViolation,
format!(
"target '{name}' assemble-markdown step is missing a string `field`"
),
)
.with_hint("add `field = \"slides\"` (or `\"transcript\"`) to the step"),
);
None
}
},
other => {
diags.push(
Diagnostic::error(
DiagCode::SchemaViolation,
format!(
"target '{name}' has a step with an invalid `type` '{other}'; \
valid values are \"typst-compile\", \"shell\""
valid values are \"typst-compile\", \"shell\", \"assemble-markdown\""
),
)
.with_hint("set the step `type` to \"typst-compile\" or \"shell\""),
.with_hint(
"set the step `type` to \"typst-compile\", \"shell\", or \"assemble-markdown\"",
),
);
None
}