Files
curriculum-project-hub/docs/adr/0015-slides-and-transcript-markdown-surfaces.md
sjfhsjfh 144e2d8c80 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>
2026-06-23 14:55:55 +08:00

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:

  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 ![](rel) image references must resolve within it, not back into the engineering root. So the assembler, after writing the markdown, scans it for local ![](rel) 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 compilecheck'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.