# ADR 0011: Export Builds Are Ordered Typed Steps Over Template Files

## Status

Accepted. **Refines ADR-0009** (which established target = a build producing a
typed artifact) by fixing the *shape* of a build and of an artifact. **Corrects
ADR-0008** on one point of fact (see *Correction* below). Some items deferred;
see *Open Questions*.

## Context

ADR-0009 said an export target is a build producing a typed `Artifact`, with a
map+reduce structure and a "declarative schema + shell hatch" form. The first
implementation of that idea (and its initial Lean encoding) got three things at
the wrong level of abstraction, surfaced in review:

1. **The `Artifact` type was a bare enum** (`singleFile | fileTree`) with no
   fields. But "what the product *is*" — a single file at some path, vs. a tree
   of files under some root — is exactly the non-obvious semantics that must be
   pinned. A bare enum erases it.
2. **A target was modeled as a loose config bag** (an artifact plus a per-kind
   render map) with presentation knobs (heading numbering) stuffed into
   `manifest.toml`. But a build is naturally **an ordered list of steps**, each a
   typed operation; and presentation belongs in a **template file**, not the
   manifest.
3. **A false premise about typst drove the design.** The build was going to have
   the framework *generate* a typst entrypoint with **static** `#include`s of
   every element, on the stated belief that "typst forbids a runtime-computed
   include/import path." That belief is **wrong** (verified below). It is the
   reason ADR-0008 mentions "the generated driver" and "static `#import`s" — that
   wording is a residue of the false premise and is corrected here.

## Correction: include/import paths may be runtime-computed

Verified against the typst source (`/Users/sjfhsjfh/Typst/typst`, 0.15):

- `#include` and `#import` parse an **arbitrary code expression**, not a string
  literal (`typst-syntax` parser uses `code_expr` for both).
- The source operand is **evaluated at runtime**
  (`typst-eval/src/import.rs:22` for import, `:174` for include); a
  `Value::Str` is then resolved to a module via runtime path resolution
  (`span.resolve_path(...)`). Path resolution is a **runtime** operation, so a
  missing file is a runtime error, not a parse error.
- The test suite confirms it: `tests/suite/scripting/include.typ` contains
  `#let chap2 = include "modu" + "les/chap" + "2.typ"` — a computed path.
- The **only** restriction: a *bare* `import expr` (no `as name`) rejects a
  dynamic string source ("dynamic import requires an explicit name");
  `import expr as name` and `include expr` both accept dynamic paths.

**Consequence:** the framework does **not** need to generate a static-include
driver. A template can `toml()`-read the manifest and `include` each part's
content via a **computed** path. This removes an entire generated-artifact layer.

## Decision

### `Artifact` is an ADT with fields

```
SingleFile { filepath }            -- the product is one file, written at filepath
FileTree   { root, outputs:glob }  -- the product is the files under root matching outputs
```

`SingleFile.filepath` says where the one product lands (a 讲义/教案 PDF).
`FileTree.root` is the output directory; `FileTree.outputs` is a **glob**
describing which files this build produces (e.g. `**/*.{html,js,json}`) — lighter
than an explicit manifest of outputs, while still letting a consumer/checker know
what to expect and verify. These fields are the semantics; they go in the Lean
ADT with doc, not erased behind a bare tag.

### A target is an artifact plus ordered, typed steps

A target's build is `{ artifact, steps }`, where `steps` is an **ordered** list
and each `Step` is a **typed operation** (extensible):

- `typstCompile { template }` — compile a **template file** (e.g.
  `exports/student.typ`) to the artifact. It is *typed*, not a raw shell line,
  precisely because the framework must **wire the manifest into it** — a bare
  `typst compile` shell string cannot express that injection.
- `shell { run }` — an escape hatch for steps that resist declaration (this is
  where ADR-0005's medium-only category (b): HTML interactive builds, npm, lands).

MVP has exactly one step per target (a single `typstCompile`), but the structure
is a list because a `FileTree`/third-party build will need several.

### Presentation lives in the template file, not the manifest

The `typstCompile.template` (e.g. `exports/student.typ`) is a **real, editable
file** in the engineering file. Presentation — heading numbering (numbly), styling
— lives **there**, not in `manifest.toml`. The manifest's earlier
`[targets.*.numbering]` block is **removed**. (Framework defaults for these
templates are written into the engineering file at project-creation time; the
creation flow is out of this round's scope — the sample's templates are authored
by hand and noted as "should be seeded".)

### The manifest is injected, not statically inlined

A `typstCompile` step compiles its template **as the main file**, passing the
manifest in via `--input manifest=<path>` (a path relative to the typst `--root`,
which is the engineering-file root). The template does
`toml(sys.inputs.manifest)` to read metadata (course title, author) and the
ordered `parts`, and `include`s each part's content via the part's path (a
**computed** path — legal per the *Correction*). No framework-generated driver.

### Render coverage is a declaration, not a payload

ADR-0009's per-target render *payload* (`RenderRule`) is **retired**: the "how" of
rendering now lives in the template/steps, not in a contract-level rule object.
What the contract keeps is a **coverage declaration** — *which kinds a target
renders* — used only by the seed diagnostic. ADR-0005's rule survives unchanged:
a used kind a target does **not** cover ⇒ a `renderIgnored` **warning** (non-
blocking). `Primitives.RenderRule` is removed from the model.

## Consequences

- The product type is self-describing: `SingleFile`'s path and `FileTree`'s
  root+glob are in the contract; `cph build` and any consumer know exactly what a
  target emits and where.
- A build is a list of typed steps — adding a non-typst step (npm, packaging) is
  adding a `shell` step, not bending the typst path.
- Presentation is editable in one obvious place (the template file); the manifest
  carries structure (parts, targets, artifacts, steps), not styling.
- Removing the generated-driver layer simplifies the engine: it compiles a real
  template file as main (spans resolve to a real authored file, not a synthetic
  one) and sets one `sys.inputs` value.
- ADR-0008's "generated driver / static `#import`s" wording is superseded by the
  template-injection model here.

## Open Questions / Deferred

- **`FileTree` and `shell` step implementation** — specified here, not built this
  round (no third-party target instance yet). MVP implements `SingleFile` + a
  single `typstCompile` step; the rest is deferred with explicit diagnostics.
- **Project-creation seeding** — that default templates are written into a new
  engineering file at creation time is asserted but not implemented here; the
  sample's templates are hand-authored.
- **Template ↔ framework calling convention** — how exactly the template pulls
  part content (a `cph-render` helper that takes a loader closure, vs. the
  template doing its own `for`+`include`) is an implementation detail settled in
  the render package, not pinned in Lean.
- **Multiple steps / step dependencies** — ordering is fixed as a list; whether
  steps ever need richer dependency structure is deferred until a multi-step
  target exists.
