# ADR 0013: Shell-Step Execution For Tool-Generated Assets ## Status Accepted — and implemented. The `Step::Shell` escape hatch (introduced structurally in ADR-0011 but left unimplemented) now has an executor in `cph-check`, wired into `cph build --target `. First real use: the KenKen interactive boards of the 恒一小奥 lesson, generated by the external `kendoku` CLI. Builds on **ADR-0005** (category (b): medium-only information — HTML interactive behavior, build scripts, npm deps — belongs to the target's build, not the element) and **ADR-0009/0011** (a target is an `Artifact` produced by an ordered list of typed `Step`s; `Shell` is the escape hatch for steps that resist declaration). It does not amend them; it fixes the **execution semantics** of `Shell`, which those ADRs left open ("MVP 不实现,先建结构"). ## Context Authoring the 恒一小奥 KenKen lesson as a native engineering file surfaced a concrete category-(b) need. The lesson's interactive teaching apparatus — a clickable 4×4 KenKen board per problem — is produced by a **separate tool** (`kendoku`, a TypeScript CLI living in the playground: `kendoku export -o `). The structured source of truth is the puzzle JSON (cages / solution / cell-to-cage); the self-contained interactive HTML is a **generated build artifact**, exactly ADR-0005's category (b) and ADR-0009's "third-party archive that runs an external build". The contract already named this (`Step::Shell`, `Artifact::FileTree`), and `cph-model` already parsed both, but the engine refused them: there was **no step executor**, and `cph build` only knew how to compile a typst template to a single PDF. So the lesson's apparatus could be *described* but not *produced* through the same framework. Three questions about *how* a shell step runs were genuinely undecided, and divergent answers would matter (safety, what `check` means, where failures go). ## Decision ### A shell step executes the command, in the engineering root, on explicit build `cph build --target ` for a target whose steps are `Shell` runs each `run` string through the platform shell (`sh -c` / `cmd /C`) with the **engineering root as the working directory**, so manifests use root-relative paths. cph **executes** the declared command and reports the outcome; it does **not** assemble or post-process the tool's output — the external tool writes the files (this is the difference from a `TypstCompile` step, where the framework owns the compile). ### Arbitrary command execution is opt-in by construction A shell step is arbitrary code execution, so it must never be a surprise: - It runs **only** on an explicit `cph build --target `. - `cph check` **never** runs shell steps — `check` validates lesson *structure* (is the lesson legal?), not the outcome of running an external tool. A shell/file-tree target is also skipped by the check pipeline's typst-compile phase (it is not a typst build). - The CLI prints each command before running it, and streams the tool's output. ### A shell step failure is a build-process error, not a lesson diagnostic A non-zero exit is a failure of the *build process*, categorically distinct from the six lesson-legality diagnostics (`PartPathMissing`, `UnknownKind`, `MissingContentFile`, `SchemaViolation`, `TypstCompile`, `RenderIgnored`). Those six describe defects *in the lesson*; a tool that fails to run says nothing about the lesson's legality. **We deliberately do NOT add a `ShellStep` diagnostic code.** The closed, spec-pinned taxonomy (`Check/Diagnostic.lean`, ADR-0010/0012) stays at six. Shell outcomes are reported by the build-execution layer (exit status + captured stdout/stderr surfaced by the CLI), keeping the diagnostic vocabulary about lesson legality only. This is the lighter, constitution-respecting choice (taxonomy is a pinned divergence point; not every error category belongs in it). ### `kendoku` reachability is left open How the external tool is invoked (a relative `node …/dist/cli.js` path, `npx kendoku` after `npm link`, or an installed binary on PATH) is **not** fixed by the contract — it is a property of a given engineering file's `run` string and its host. The 恒一小奥 manifest uses a relative `node` path as an MVP and its README documents the prerequisite; a portable convention is left open. ## Consequences - The same framework now *produces* the apparatus it could previously only describe: `cph build --target interactives` runs `kendoku` and writes the 10 interactive boards under the target's `FileTree` root. The lesson's PDFs (`student`/`teacher`) and its interactives all come from one engineering file through one CLI. - `Step::Shell` is no longer a placeholder; its execution semantics are pinned in `Export/Render.lean`. - The diagnostic taxonomy is unchanged (still six). Build-process failures live outside it. - `TypstCompile` targets are unaffected: the PDF path is exactly as before. ## Open Questions / Deferred - **Portable tool reachability.** A cross-machine convention for locating an external generator (`npx`/PATH/vendored) rather than a host-specific path. - **`FileTree` output validation.** Whether the checker should verify a shell target actually produced the files its `Artifact::FileTree` `outputs` glob declares (currently the tool's exit status is the only signal). - **Step animation (GIF) pipeline.** `kendoku steps` → headless-Chrome screenshots → ffmpeg is a multi-tool chain; this lesson uses pre-generated static step PNGs instead. Wiring that chain as shell steps is deferred.