forked from bai/curriculum-project-hub
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]] 序拼、写盘后
收集  引用图进 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:
@@ -0,0 +1,177 @@
|
||||
# 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:
|
||||
|
||||
1. **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;
|
||||
`mitex` is LaTeX→typst, the reverse direction — ADR-0014 §Context).
|
||||
2. **slides/逐字稿 can be authored directly in markdown.** If `slides` and
|
||||
`transcript` are 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 (the `interactives` shell
|
||||
target writes `build/interactives/`), not engineering-root sources, so they are
|
||||
referenced but **not** copied by the assembler. A fully self-contained `build/`
|
||||
therefore requires running the `interactives` target first — a documented
|
||||
prerequisite, like `kendoku` reachability (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:
|
||||
|
||||
1. **opt-in by construction** — runs only on an explicit
|
||||
`cph build --target <name>`, never in `check`. `check` validates lesson
|
||||
*structure*, not the outcome of assembling markdown.
|
||||
2. **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
|
||||
`AssembleMarkdown` diagnostic code (taxonomy stays 6, ADR-0010/0012/0013).
|
||||
3. **non-typst targets skip typst compile** — `check`'s compile phase already
|
||||
filters to `TypstCompile`-bearing targets, so a pure `assembleMarkdown`
|
||||
target 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 `Step` variant (`assembleMarkdown`) joins `typstCompile` and
|
||||
`shell`; the step universe is extensible by design (ADR-0011).
|
||||
- The `content` leaf concept broadens: it now carries a *format*. The
|
||||
`x-cph-content` marker accepts an extension (`"md"`), backward-compatible
|
||||
with the boolean form (`.typ`). `RichContent.lean` pins `ContentFormat`.
|
||||
- The 6-class diagnostic taxonomy is unchanged.
|
||||
- The 恒一小奥 KenKen lesson gains `slides`/`transcript` targets alongside its
|
||||
existing `student`/`teacher`/`interactives` targets — 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`/`transcript` are
|
||||
declared per-kind (4 schemas, duplicated) to match ADR-0005's "(a)-class
|
||||
lives in kind `ElementData`". Whether they should become a cross-cutting
|
||||
universal content surface is reconsidered alongside the parts tree-restructure.
|
||||
Reference in New Issue
Block a user