diff --git a/docs/adr/0029-engineering-file-nested-manifest.md b/docs/adr/0029-engineering-file-nested-manifest.md new file mode 100644 index 0000000..64c25ae --- /dev/null +++ b/docs/adr/0029-engineering-file-nested-manifest.md @@ -0,0 +1,193 @@ +# ADR 0029: Engineering-File Structure Is A Nested Outline Manifest + +## Status + +Accepted. **Supersedes ADR-0008** on the concrete layout of a lesson's +structure (the ordered `[[parts]]` arrangement), and discharges the +"grouping/sectioning" and "manifest richness" gaps ADR-0008 left Open. ADR-0007 +(the engineering file is a real directory tree) stands unchanged. ADR-0005's +"a lesson is an ordered sequence of element instances" stands unchanged — order +and membership are preserved; the *shape* that encodes them is now a tree. + +## Context + +ADR-0008 encodes a lesson's order and membership as a **single flat `[[parts]]` +array** in a root `manifest.toml`, where every `[[parts]]` entry is a `kind` + +`path` to an element folder. Two forces now push against that flat shape: + +1. **Real lessons are internally structured.** TH-144 has a `题目/` tree of + problem/answer pairs and A/B/C sections that exist only in folder names + today. Teachers think of a lesson as an **outline** — sections, sub-sections, + groups of worked examples — not an unbroken flat list of ~40 parts. The + admin/teacher surface (the 老师端 being built against the Hub) is supposed to + show "the project structure, expanding each structural element to the files + inside" — and today that structure is a giant flat scroll. +2. **Outline and file structure should correspond 1:1, not via a separate + index.** The 7.31 design discussion landed on a shape where each level of the + lesson is a folder whose manifest states that level's children — so the + on-disk tree *is* the outline, self-descriptive, with no secondary artifact + to drift out of sync. A flat root `[[parts]]` list, by contrast, names the + whole lesson in one file and forces the folder tree to be a projection of it + (or vice versa) with two sources of truth. + +ADR-0008 itself anticipated this: its Open Questions list "Per-part metadata, +grouping/sectioning (TH-144's A/B/C structure is only in folder names today)" as +explicitly not modeled. This ADR closes that gap. + +## Decision + +### A lesson is a tree of folders; each folder is a self-describing node whose manifest names its children + +The engineering file remains a real directory tree (ADR-0007). The ordering and +membership encoding of ADR-0008 changes from **one flat root `[[parts]]`** to a +**nested, per-folder outline**: + +- The root's `manifest.toml` keeps `[project]`, `[info]`, and the `[targets.*]` + build configuration exactly as ADR-0008/0011 define them. +- The lesson's **structure is expressed as a folder tree**, where every folder + that groups children carries its own small **outline manifest** (per-folder + `manifest.toml`, see *Name and discriminator* below) stating that level's + ordered children. +- A **leaf** is an element folder exactly as ADR-0008 defines it: an + `element.toml` declaring `kind` + scalar fields, plus convention-named + content `.typ` siblings. A leaf has no outline manifest — its own + `element.toml` is its descriptor. +- An **internal folder** (a grouping node) has an outline manifest but no + `element.toml`: it is not an element, it is a container of elements/containers. + It carries only structure and, optionally, group-level scalar metadata. + +### The engineering-file root is itself the implicit top container + +Children live directly in the root `manifest.toml`'s `children` array — there is +no mandated single top-level section folder. Rationale: migration is a pure +flatten of the existing root `[[parts]]` into root `children` (same order, zero +forcing); a mandated wrapper folder would be pure indirection for most lessons. +A lesson that wants a top-level section simply creates one as a child +(consistent with the ADR-0021 folder-tree precedent, which holds direct children +at the root). + +### Name and discriminator: every grouping folder uses `manifest.toml` + +Every folder that groups children uses the same filename, `manifest.toml`, at +every level including the root: + +- Root `manifest.toml`: `[project]`, `[info]`, `[targets.*]`, plus a `children` + array. +- Internal folder `manifest.toml`: `children` (+ optional `[group]` scalars); + never `[project]`/`[info]`/`[targets.*]`. + +The leaf/container discriminator is disjoint and structural: a folder with +`element.toml` is a **leaf** (ADR-0008 descriptor); a folder with `manifest.toml` +and no `element.toml` is a **container**; a folder with neither is a structural +error. `OUTLINE.toml` was rejected (a new reserved name, no benefit over the +uniform name); `info.toml` was rejected because it collides with the model's +`Info` (title/author, folded into root `manifest.toml`'s `[info]` by ADR-0008) +and would blur "metadata vs structure". The 7.31 sketch's intent — each level +self-describes its children — is preserved; the name aligns with current +ADR-0008. + +### Order is encoded per-folder, and the lesson order is the depth-first traversal + +ADR-0005 requires the lesson to be an ordered sequence. In the tree, **order is +declared locally at each folder** by the order of children in that folder's +`manifest.toml`. The canonical lesson order is the **depth-first pre-order +traversal** of the tree: an internal folder contributes no element of its own +(its label is a heading, not a part), and leaves contribute in the order they +appear. The checker materializes this traversal; no part of the lesson order +lives in a typst script (ADR-0008's core rejection of typst-as-order-manifest +stands). + +Concretely, a `segment` that in TH-141 was one flat `[[parts]]` entry can now be +a folder whose outline lists its sub-segments and examples in order — and any +grouping (TH-144's A/B/C, a "导言 cluster", a "例题组") is a folder, transparent +in the element sequence but a real node in the outline. + +### The outline manifest shape + +A folder's `manifest.toml` `children` array holds its ordered children. Each +child entry is either: + +```toml +# a leaf element (ADR-0008 descriptor), by relative path +{ kind = "example", path = "examples/41届复赛三-1-混注石油" } + +# or an internal grouping folder, by relative path (recursed) +{ kind = "section", path = "导言簇" } +``` + +The `kind` of a **leaf** is still read from that folder's `element.toml` +(ADR-0008: the folder is self-describing; the outline entry may restate it for +readability but the `element.toml` is authoritative). The `kind` of an +**internal** child is a container kind — recognized from a small, open set of +container kinds — and selects how that subtree is rendered/grouped. Leaves and +containers are disjoint by construction: a folder is a container iff it has a +`manifest.toml`; a leaf iff it has an `element.toml`. A folder must have exactly +one of the two. + +### Container kinds: MVP ships exactly one — `section`, rendered as a heading + +The demonstrated needs (TH-144's A/B/C, a "导言簇") are all `section`, so the +MVP ships exactly one container kind: + +- A `section` opens a **heading** at its depth in the DFS, then renders its + children in order; it never appears in the element part sequence (the + already-decided DFS semantics). +- The heading uses `[group].title` when present, else the folder name. +- `group` (a heading-less visual grouping) and any other container kind are + **deferred**: added only when a real need appears, honoring ADR-0005's open + universe / "add when needed". + +### Group-level scalars + +An internal folder MAY carry a `[group]` table (e.g. a title distinct from the +folder name, a description) in its `manifest.toml`. Kept minimal — no other +container metadata until a real need appears. + +### The flat `[[parts]]` array at the root is retired for structure + +The root `manifest.toml` no longer needs a root-level `[[parts]]` that names the +whole lesson. Lesson structure lives in the folder tree, rooted at the +engineering-file root's `manifest.toml` `children`. `[targets.*]` and +`[project]`/`[info]` stay at the root `manifest.toml`. + +## Consequences + +- The teacher/admin surface shows the outline: the folder tree *is* the lesson + structure, self-descriptive and 1:1 with files. Opening an element reveals its + files (as the 老师端 requirement asked). This discharges the 7.31 driver + ("项目内部有一套 cph schema 定义的结构,由 manifest 组织,给老师看的应该是这个, + 展开每个结构元素内部才是文件"). +- The checker can still recover the full ordered lesson **without evaluating + typst**: it reads the root `manifest.toml` (project/info/targets) and walks the + folder tree, honoring each folder's `manifest.toml` children order and each + leaf's `element.toml`. Order and membership remain declarative data, greppable + and diffable. +- Grouping (sections) is now a real, checkable structure rather than a folder + naming convention — TH-144's A/B/C can be first-class. +- Every folder is self-describing, so a subtree can be understood/moved on its + own; nothing about a subtree's structure lives only in the root file. +- Migration is mechanical: flatten the existing root `[[parts]]` into root + `children` with the same leaf order (root is the implicit top container, so no + wrapper folder is needed). The element sequence is unchanged, so + `cph check`/`cph build` semantics for leaves carry over. + +## Open Questions / Deferred + +- **Target-scoped container options** (e.g. hide a section in the student + build): deferred, stay out of structure. ADR-0009/0011 field-visibility / + per-target map already handles this in rendered output, not structure; keep it + there unless a concrete need forces it back into the manifest. +- **Additional container kinds** beyond `section` (e.g. a heading-less `group`): + deferred until a real need appears (open universe, ADR-0005). +- **Container metadata beyond `[group]`** (title/description): deferred — only + the minimal `[group]` table ships; richer container scalars await a concrete + authoring need. + +## Supersedes + +ADR-0008's "the ordering manifest is declarative" decision stands; this ADR +replaces its **flat `[[parts]]` encoding of order/membership** with the nested +per-folder outline. ADR-0008's other decisions (declarative `manifest.toml`/ +`element.toml`, folder self-description, content-file naming convention, schema +as source of truth for which `.typ` files exist) are unchanged and carry into +this tree form. diff --git a/docs/adr/0030-batch-and-combined-export.md b/docs/adr/0030-batch-and-combined-export.md new file mode 100644 index 0000000..1a7c8da --- /dev/null +++ b/docs/adr/0030-batch-and-combined-export.md @@ -0,0 +1,167 @@ +# 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: + +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.