ADR-0029 supersedes ADR-0008's flat [[parts]] with a per-folder manifest.toml outline tree (root = implicit top container, single section container kind, DFS pre-order = lesson order). ADR-0030 extends ADR-0009/0011: cph build batches all targets (any failure => non-zero), cph bundle assembles ordered lessons into one SingleFile/FileTree artifact (self-contained lessons, export-time combination only).
9.0 KiB
ADR 0030: 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:
-
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 buildinvocations with no shared invocation, ordering, or failure summary. Teachers preparing a lesson want "build the whole lesson in all needed forms" as one action. -
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
lessonsarray — 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 /@labelmachinery 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 (SingleFilemerged doc orFileTree), giving the multi-lesson merge. (cph buildon 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
SingleFilebundle — 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.