给课程加两个 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]] 序拼、写盘后
收集  引用图进 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>
9.9 KiB
ADR 0015: Slides & Transcript As Per-Element Markdown Content Surfaces
Status
Accepted — and to be implemented. Adds two new markdown content surfaces
(slides, transcript) as per-element optional content fields, plus a typed
Step.assembleMarkdown that concatenates them in parts order into a single-file
markdown deliverable.
Builds on ADR-0005 ((a)-class fields like 逐字稿 live inside a kind's
ElementData; (b)-class medium lives in the target build), ADR-0006
(content leaves in a kind schema), ADR-0011 (a target is an artifact +
ordered typed steps; presentation lives in the template/content not the
manifest), ADR-0013 (non-typst targets ride a build step; failures are
build-process errors outside the 6-class taxonomy), and ADR-0014 (the
markdown/HTML export direction — R1 shared typst-content vs. R2 target-specific
content; math must survive as markup not images).
Context
The KenKen lesson (and curriculum generally) needs a slides deliverable — an outline-grade projection (titles + bullet points; detail belongs in the 讲义/教案 or the 逐字稿), aimed at a third-party platform whose interchange format is a specific markdown dialect. It also needs a 逐字稿 (transcript) — the spoken text, read aloud, no typography needed.
Two prior facts made the design:
- slides content ≠ 讲义 content. ADR-0014 already recorded that a
markdown/HTML target's content does not fully overlap the PDF 讲义/教案.
Forcing slides to be a projection of the same typst source (ADR-0014's R1)
drags in the hard typst→markdown math conversion (typlite renders math
as images; 0.15 MathML is accessibility-oriented and hard to turn into KaTeX;
mitexis LaTeX→typst, the reverse direction — ADR-0014 §Context). - slides/逐字稿 can be authored directly in markdown. If
slidesandtranscriptare authored as markdown + KaTeX from the start, there is no typst→markdown conversion at all — the math is KaTeX source ($…$) on the day it is written. The math principle ("markup, not images", ADR-0014) is satisfied for free, not by building a fragile converter.
So for the slides/transcript surfaces this ADR picks ADR-0014's R2 (target-specific content) deliberately: it sidesteps the typst→md converter entirely. The converter (R1) remains an open problem only for the 讲义 surface, which is independent and out of scope here.
The spec already pins where (a)-class fields live: Element.lean says
"逐字稿、重点圈划 落在具体 kind 的 ElementData 内". So slides/transcript
are modeled as per-kind optional content leaves (declared in each kind's
JSON schema), consistent with the existing textbook/problem/solution
content fields — except their format is markdown, not typst. This is a real
extension to the content concept: a content leaf now carries a format
(typst | markdown). RichContent.lean gains a ContentFormat and the content
ref gains a format field.
Decision
Two new markdown content fields on every kind
Each stdlib kind schema (segment, example, lemma, sop) gains two
optional content fields — slides and transcript — whose format is
markdown (x-cph-content: "md", the new extension-bearing form of the
marker; the old x-cph-content: true stays .typ for backward compat).
Optional ⇒ a kind instance without them is legal (like a lemma's proof).
Convention: slides.md is outline-grade (title + bullets; no worked
detail — that lives in 讲义/逐字稿); transcript.md is the spoken text.
A new typed step Step.assembleMarkdown field
A target like [targets.slides] carries [[…steps]] type = "assemble-markdown" field = "slides" over a single-file artifact. The framework owns the
assembly (not a cat): it walks the manifest [[parts]] in order, reads
each element's <field>.md if present (skips if absent), joins them with a
blank-line separator, and writes the single file to the artifact's filepath
(relative to the engineering root). It does not inject headings — each
slides.md/transcript.md authors its own # … (presentation lives in the
content, ADR-0011).
This is typed, not shell, because ordered-read + skip-missing +
write-to-artifact-path is cleaner owned by the framework than expressed as a
shell pipeline, and it stays deterministic/testable.
Self-contained build dir: the assembler collects referenced images
The assembled slides.md is written under the build root (the artifact
filepath's parent, e.g. build/). For that directory to be a standalone
deliverable — uploadable to the platform on its own — its  image
references must resolve within it, not back into the engineering root. So the
assembler, after writing the markdown, scans it for local  references
and copies each referenced image from the engineering root (<eng-root>/<rel>)
into the build root (<build-root>/<rel>), preserving the relative path. This
is part of the assembleMarkdown step's owned responsibility (one step, not a
separate copy step), keeping a slides target a single typed step.
Conventions that fall out of this:
- Image references are authored relative to the engineering root (
assets/images/…), which is also their form relative to the build root after the copy — so the same path string is correct both at authoring time (the source image exists at<eng-root>/assets/images/…) and in the assembled output. - Widget references are build-relative (
interactives/ex1.html): widgets are build artifacts produced by a separate target (theinteractivesshell target writesbuild/interactives/), not engineering-root sources, so they are referenced but not copied by the assembler. A fully self-containedbuild/therefore requires running theinteractivestarget first — a documented prerequisite, likekendokureachability (ADR-0013). - External URLs (
http:///https:///data:) are left untouched. - A referenced image missing at the engineering root is a build-process error (the build dir would be broken), reported by the build layer — it does not enter the 6-class taxonomy.
Course-level h1 is injected
The assembler prepends the course title (from manifest [info] title) as the
document's # h1. This is course metadata, not per-element content; each
element's <field>.md then contributes its ## part and ### 小节. This is
the one piece of presentation the assembler owns at the document level (ADR-0011's
"presentation lives in the content" governs per-element presentation).
Same three boundaries as the shell step (ADR-0013)
assembleMarkdown is a non-typst target, identical in boundary to the
shell step:
- opt-in by construction — runs only on an explicit
cph build --target <name>, never incheck.checkvalidates lesson structure, not the outcome of assembling markdown. - failure is a build-process error, not a lesson diagnostic — an assembly
failure (e.g. write error) does not enter the 6-class diagnostic
taxonomy; it is reported by the build layer. We do not add an
AssembleMarkdowndiagnostic code (taxonomy stays 6, ADR-0010/0012/0013). - non-typst targets skip typst compile —
check's compile phase already filters toTypstCompile-bearing targets, so a pureassembleMarkdowntarget is naturally excluded.
Single-file artifact now; structured FileTree deferred
The artifact is a single-file markdown. The user reasoned that until parts
are organized as an ordered tree (rather than the current flat ordered
list), single-file is unavoidable — a loose FileTree of per-element .md
files would lose the ordering/structure the manifest encodes. So: single-file
now; a structured multi-file FileTree artifact is deferred until the parts
tree-restructure lands (it is out of scope here and recorded as OPEN).
Consequences
- The framework gains two orthogonal content surfaces (outline slides, spoken transcript) authored directly in markdown+KaTeX, with no typst→md math conversion in their path. The math problem (ADR-0014) is not solved generally — it is avoided for these surfaces by choosing R2.
- A third typed
Stepvariant (assembleMarkdown) joinstypstCompileandshell; the step universe is extensible by design (ADR-0011). - The
contentleaf concept broadens: it now carries a format. Thex-cph-contentmarker accepts an extension ("md"), backward-compatible with the boolean form (.typ).RichContent.leanpinsContentFormat. - The 6-class diagnostic taxonomy is unchanged.
- The 恒一小奥 KenKen lesson gains
slides/transcripttargets alongside its existingstudent/teacher/interactivestargets — all from one engineering file through one CLI.
Open Questions / Deferred
- Third-party markdown dialect. Baseline CommonMark + KaTeX (
$…$/$$…$$) ships now; the platform's specific dialect (page-break markers, callout / fenced-div conventions, heading-depth rules, math-delimiter quirks) is a transform layer to be added once the lesson's output is fed to the platform and the real requirements are observed. - Structured FileTree artifact. A multi-file markdown deliverable organized by the parts tree — deferred until the parts tree-restructure (itself a separate OPEN / future ADR; touches the manifest schema, render assembly, and all examples).
- 讲义 (typst) → markdown. Still ADR-0014's open R1/converter problem, unaffected by this ADR. Whether the 讲义 surface ever needs a markdown export is an independent decision.
- Universal vs. per-kind markdown surfaces.
slides/transcriptare declared per-kind (4 schemas, duplicated) to match ADR-0005's "(a)-class lives in kindElementData". Whether they should become a cross-cutting universal content surface is reconsidered alongside the parts tree-restructure.