Files
curriculum-project-hub/docs/adr/0037-batch-and-combined-export.md
T
ecc92c9a87 docs(adr): cph 的 nested-manifest / batch-export 改号 0036/0037
上游 0029/0030 与本地 filelib 侧同号 ADR 相撞,cph 两份改到本地空闲号段,
代码锚点引用一并跟随。
2026-08-06 00:51:33 +08:00

9.0 KiB

ADR 0037: Batch & Combined Export

Status

Accepted. Extends/refines ADR-0009 and ADR-0011 (export target = a build producing a typed artifact) by adding two export dimensions that today have no home: (1) building multiple targets of one lesson in one batch, and (2) combining multiple lessons into one artifact — a 讲义合集 / course bundle. It does not redefine the SingleFile vs FileTree artifact distinction (ADR-0011) or the single-typstCompile-step MVP; it adds the collection semantics on top.

Context

Today cph build --target T builds exactly one target T of one engineering file into one artifact. Two real needs fall outside that:

  1. A lesson's multiple versions. A lesson already declares several targets (student handout, teacher plan, slides, script). Producing all of them is today N separate cph build invocations with no shared invocation, ordering, or failure summary. Teachers preparing a lesson want "build the whole lesson in all needed forms" as one action.

  2. Combining lessons into one deliverable. ADR-0005 deferred "course = arrangement of lessons" — a course/unit is not an engineering file; it is an arrangement of lessons "modeled elsewhere". The elsewhere is empty. A real deliverable is a 讲义合集 / course bundle — several lessons ordered into one document (e.g. "期中复习合集", a term bundle, a topic compilation). This spans multiple engineering files and currently has no model and no CLI path.

Both are product-plain features (the 老师端 exports; a bundle is what a teacher hands a class), not architectural speculation.

Decision

The existing single-lesson single-target build is the atomic unit

ADR-0009/0011's model — a target is a build over one lesson producing one Artifact via ordered Steps — is unchanged and remains the unit. Nothing below replaces it; the new semantics are aggregations over that unit.

Dimension 1 — Batch: build a set of targets of one lesson

cph build on a lesson gains the ability to produce several targets in one invocation, as one batched operation:

  • The lesson root manifest.toml [targets.*] already enumerates the declared targets and their order (ADR-0008/0011). Building "all declared targets" is the default batch: each declared target builds to its own artifact (build/<target>.{pdf,md}), in declaration order.
  • A batched build is non-transactional and independent per target: each target is a separate build with its own artifact, own diagnostics, own exit/result. One target failing (e.g. teacher plan PDF) does not block the rest (student PDF), matching the per-target independent-failure stance of ADR-0009's "missing render for a used kind ⇒ warning, non-blocking".
  • The batch emits a summary: a per-target ledger (ok/failed + its artifact or error), and a non-zero aggregate exit if any target failed to produce its artifact. A target that fails to produce its artifact is a real defect (this repo's fail-fast stance — don't paper over bugs), distinct from ADR-0009's "missing render rule ⇒ warning" (a policy-level skip, non-error). "Don't block the rest" still holds: every target is attempted, but a single failure makes the aggregate non-zero so CI/observability catch it. This is the CLI's job; it is the natural "build the whole lesson" affordance.

Dimension 2 — Combined: arrange multiple lessons into one artifact

A course bundle is a new, lightweight, second kind of engineering-file-adjacent unit: an ordered arrangement of lessons (ADR-0005's deferred "course = arrangement of lessons" finally given a concrete export home).

Bundle carrier: a directory containing bundle.toml

A bundle is a directory containing bundle.toml. A directory gives the bundle a stable root for relative lesson paths and a home for build output (echoing ADR-0007's "engineering file = directory"). The bundle.toml carries:

  • [info] — the bundle's own title/author (of the 合集);
  • [targets.*] — the bundle's build configuration, reusing ADR-0011's build mechanism;
  • an ordered lessons array — each entry: a lesson path (relative to the bundle root, pointing at each engineering-file root, each a self-contained directory tree per ADR-0007) plus optional per-lesson per-target overrides.

A bundle target produces an ordered concatenation/assembly of the lessons' artifacts or content into one Artifact, reusing the ADR-0011 artifact ADT:

  • SingleFile — a combined document (讲义合集): the lessons' content assembled in order into one compiled document, with the existing cross-reference / @label machinery working because it is one compiled document (the same reason ADR-0011 gives for why SingleFile concatenation works at all).
  • FileTree — each lesson to its own file plus a generated index (ADR-0011's third-party-archive case, now with a first-class multi-lesson trigger).

A bundle target's steps are the ordered typed steps of ADR-0011, but the "map" now operates at the level of whole lessons rather than a single lesson's parts: a step like assembleLessons (ordered inclusion of each lesson's content/artifact) plus the existing typstCompile/shell steps for assembly and any post-processing. Concretely the framework provides a typstCompile-style step that pulls each listed lesson's content in order into one document (mirroring how a single lesson's template pulls its parts).

Renumbering in a SingleFile bundle: template-resident, default reset per lesson

Numbering is presentation, which ADR-0011 already owns to the template file (not the manifest), so the bundle target's template decides whether auto-counters reset at lesson boundaries; the framework ships a helper to reset counters at a lesson boundary. The recommended default resets auto-counters at each lesson boundary: lessons are authored self-contained, so an internal "例题3" means that lesson's 例题3; cross-lesson continuation would silently break author references. A genuine "全书 continuous numbering" is an explicit template override. @label cross-references stay global (resolved by label name, independent of counters); only auto-increment counters reset.

Bundles do not nest (MVP)

A bundle references lessons only, not other bundles, until a real need appears — mirrors the tree-nesting simplicity and keeps the first bundle target minimal.

Invariant: lessons stay self-contained; combination is export-time only

Opening the door to combining lessons must not open the door to cross-lesson imports inside a lesson's own content (ADR-0006's import boundary: within one engineering file plus @package, never into a sibling lesson). A bundle is allowed to assemble already-authored lessons at export time — reading their content for the combined artifact — but no lesson's rich content may import another lesson's internals as part of its own authoring. Combination is a projection over self-contained lessons, exactly as a render target is a projection over a lesson. This keeps each engineering file independently checkable, buildable, and movable, and avoids reintroducing cross-file coupling ADR-0006 explicitly rejected.

CLI surface

  • cph build <lesson> → all declared targets (the batch default).
  • cph build <lesson> --target student --target teacher → the named subset (the multi-version batch), in the given order.
  • cph bundle <bundle-path> --target <name> → build one combined bundle target (SingleFile merged doc or FileTree), giving the multi-lesson merge. (cph build on a bundle root is the batch-of-bundle-targets equivalent.)

Consequences

  • One lesson, many versions is one command with a per-target ledger — the natural 老师端 "导出全部版本" action, and any single failure surfaces as a non-zero aggregate.
  • Course = arrangement of lessons gets a concrete, export-focused home (the bundle), discharging the ADR-0005 deferred item without inventing a full course-authoring model.
  • The artifact/distinction and build-step machinery (ADR-0011) is reused — a bundle target is just a build whose inputs are whole lessons, not a new parallel export engine.
  • Self-containment stays (ADR-0006/0007): each lesson remains independently checkable and buildable; the bundle only reads them for assembly. A lesson and a bundle can version/evolve independently.
  • The teacher surface can offer "export all versions" (batch) and "compile into a 合集" (combined) as two concrete, productisible actions.

Open Questions / Deferred

  • Bundle-of-bundles / nesting — no nesting in MVP; re-open only when a real need appears.
  • Dedup/caching across targets and lessons — none in MVP, consistent with ADR-0011's "no caching in MVP".
  • Exact counter-reset semantics in a SingleFile bundle — the default (reset per lesson) is decided; the precise mechanism (which counters, how the template override is expressed) is settled with the first bundle template implementation.