Files
curriculum-project-hub/docs/adr/0013-shell-step-execution-for-tool-generated-assets.md
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

5.5 KiB
Raw Permalink Blame History

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 Steps; 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.