Files
curriculum-project-hub/docs/adr/0013-shell-step-execution-for-tool-generated-assets.md
T
sjfhsjfh 9590e15236 feat(checker+typst): 嵌入 render 使 cph 可 cargo install;实现 shell step 执行器(ADR-0013)
三件事,服务于"把恒一小奥 KenKen 课作者化为合法 native 工程文件"这条主线
(工程文件本身在 repo 外的 sibling playground,不入此提交):

1. cph-typst: 把 render/ 编译期嵌入二进制,`cargo install` 后开箱即用
   - build.rs 把 render/ 拷进 OUT_DIR,剔除 dev 自指符号链接 vendor/local-packages
     (否则 include_dir! 无限递归)
   - embedded.rs 用 include_dir! 嵌入,运行时按版本解压到 per-user cache dir
     (CPH_RENDER_DIR 仍优先作 dev override)
   - default_render_dir 不再用编译期 CARGO_MANIFEST_DIR 写死路径

2. cph-check + cph-cli: 实现 Step::Shell 执行器(此前仅占位)
   - run_shell_target: 跑 check 门控 → 以工程根为 cwd 执行 shell step,首非零退出即停
   - target_is_shell + CLI run_shell_build 路由;`check` 不跑 shell(结构校验只看 lesson)
   - compile_targets 只保留含 TypstCompile 的 target,纯 shell target 不走 typst 引擎
   - 语义:失败属 build-过程错误,不入 6 类诊断(taxonomy 保持 6,显式拒绝加 ShellStep 码)

3. spec + ADR: 在 Export/Render.lean 钉 shell step 执行语义 prose;
   ADR-0013(已实现)、ADR-0014(md/HTML 导出后端方向,Proposed/设计先行,未实现)

测试:全 workspace 42 passed(新增 4 个 shell 执行器测试);lake build 25 jobs 绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:09:49 +08:00

107 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <shell-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
<puzzle.json> -o <out.html>`). 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 <name>` 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 <shell-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.