Files
curriculum-project-hub/docs/adr/0036-engineering-file-nested-manifest.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

10 KiB

ADR 0036: 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:

# 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.