# 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 `Step`s — 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/.{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 ` → all declared targets (the batch default). - `cph build --target student --target teacher` → the named subset (the multi-version batch), in the given order. - `cph bundle --target ` → 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.