Appendix D — Pre-1.0 checklist
Appendix D. Pre-1.0 checklist (informative)
Informative. This appendix tracks language-level frictions and design tensions known to need resolution before the Cook Standard is declared stable at 1.0. Each entry names a concrete symptom, the workaround in current use, and the resolution path.
D.1. Conformance corpus is parser-impl-coupled
Symptom. The parse.txt files under standard/conformance/positive/ are textual dumps of one specific implementation’s AST shape. When that implementation changes its dump format — for example, CS-0011 dropped the vars: [] line in v0.2 — a previously-tagged corpus does not match the current parser, breaking the backwards-conformance recipe (cook standard.against-tag, which routes through standard/lua/checks.lua) against any prior tag whose dump predates the change.
Workaround. When a parser change alters the dump format, the cs-standard/vX.Y tag for the in-progress version is force-moved to the merge commit where the parser and the corpus agree (operating rule in CONTRIBUTING.md’s cut procedure). Earlier tags are accepted as broken; their corpora remain frozen historical records.
Resolution path. Either move parse.txt out of the normative corpus into impl-specific fixtures (each implementation owns its own dump checks), or define an impl-agnostic structured AST format (e.g., JSON of a normative shape) so historical tags stay verifiable.
D.2. ${...} syntax in using strings
Status. Fully resolved in v0.5 by CS-0022 (for the using surface) and CS-0024 (for the plate and test surfaces).
Original symptom. Cook’s templater inside using "..." strings interpreted ${VAR} as its own env-lookup form, breaking Bash parameter-expansion forms like ${VAR:-default}.
Resolution. CS-0022 removed the single-string using "..." form in favour of { … } shell blocks. Inside a shell block, { and } are brace-balanced by the Cookfile lexer (§ 3.9) and the block content is passed verbatim to the shell — Cook’s templater never sees the interior. ${VAR:-default}, ${VAR%suffix}, and all other Bash parameter-expansion forms work inside a { … } block without any workaround.
Residual scope. The Fully resolved in CS-0024: both plate "..." string form is still a Cook-templated string and retains the original symptom.plate and test now exclusively use the brace-block body form (plate { … }, test { … }); the Cook-templated string form has been removed from the language surface.
D.3. Cook strings have no " escape
Status. Substantially resolved in v0.5 by CS-0022 (for the using surface) and CS-0024 (for the plate and test surfaces); open for the remaining STRING positions (recipe names, input paths, cook output patterns).
Original symptom. The string literal in cook "...", using "...", plate "...", recipe "...", etc. is parsed by a routine that finds the first " and stops — \" is not recognized as an escape. Embedding a literal " inside any cook string truncates the string at the first inner ", producing surprising and silent behaviour (the truncated command runs, the rest of the line is leftover content).
Resolution (partial). CS-0022 removed using "...". Shell content that previously had to live in a Cook-string is now written inside a { … } block, where the shell’s own quoting rules apply and " is legal in any shell-valid position.
Residual scope. Recipe names, input paths, and cook output patterns still use unescaped STRING (§ 3.5); a " in any of those positions remains inexpressible. The resolution path is unchanged. Fully resolved in CS-0024: plate "..." and test "..." strings still use unescaped STRING.plate and test now use the brace-block body form exclusively, removing them from the set of STRING surfaces with this limitation.
Workaround (unchanged). Avoid embedding double quotes in cook strings. When shell-quoting is genuinely needed in a command, use the >{ … } Lua-block form: Lua handles its own quoting and cook.sh accepts arbitrary strings.
D.4. tree-sitter-cook is stale
Symptom. tree-sitter-cook/grammar.js lags the Cook Standard. The grammar header currently claims cs-standard/v0.4 + CS-0022; the Standard is at v0.7, so the grammar is missing every change cut after CS-0022 — notably CS-0024 (plate/test block-body unification), CS-0026 (parse-time §7.2 import-path enforcement), CS-0027 onward (cross-Cookfile / cache-cloud-readiness work), CS-0032 (//-anchored sigil imports), and CS-0033 ($<IDENT> placeholder syntax). Earlier closure of CS-0011 (top-level NAME "value" rejection), CS-0018 (name: recipe header dropped), CS-0019 (implicit termination), and CS-0020 (chore) is in tree, so the v0.2/v0.3 gap that originally motivated this entry is closed; the live gap is v0.4 → v0.7.
Workaround. The grammar’s tree-sitter test corpus passes against its own (stale) view of the language. The Standard does not require tree-sitter conformance pre-1.0; the cook-lang Rust parser is the reference implementation.
Resolution path. The follow-up plan referenced by CS-0002 will: enumerate divergences, update grammar.js and externals, wire a tree-sitter conformance harness against standard/conformance/, and verify both implementations agree on the corpus. Each subsequent Standard cut should re-open the gap measurement against the latest version banner in grammar.js.
D.5. Implicit termination of recipe and config bodies
Status. Resolved in v0.4 by CS-0019.
Tension. Each recipe and config block must close with an explicit end line. The closing keyword is ceremony — every author writes it without thinking, and the parser has no need for it that couldn’t be served by “next top-level construct or EOF.” A Cookfile with ten recipes has ten lines that exist only to satisfy the grammar.
Status quo. § 6 (recipes) requires end at the close of every recipe body; § 5.3 requires it for config blocks. App. A.3 enumerates end in the recipe production. The lexer produces Token::RecipeEnd for column-0 end lines; the parser consumes one per body.
Constraints. The brace-block-as-opaque-source-lines model already cleanly separates Lua- and shell-block boundaries from recipe boundaries — a recipe keyword appearing inside a Lua block’s source is never seen by the recipe-body parser, because the brace-counter skips the token stream over the block. Implicit termination by next top-level keyword does not interfere with embedded blocks. The lexer currently classifies recipe/config/use/import keywords on the trimmed line content (any indentation), which would let an indented recipe foo silently close an outer recipe under the new rule. Tightening the lexer to require column 0 for those keywords is part of the resolution.
Possible resolution. Drop end from the recipe and config grammars. Body extends until the next column-0 line classified as recipe, config, use, or import, or EOF. Lexer changes: require column 0 for top-level keywords; remove Token::RecipeEnd (a bare end line then becomes Token::Content and falls into the existing “unexpected content outside a recipe” path or, inside a recipe, into a shell command). Conformance corpus migrates by stripping end lines and updating parse.txt line counts. Diagnostics for “you forgot to close this recipe” largely disappear because there is nothing to forget.
D.6. Implicit recipe header form
Status. Resolved in v0.4 by CS-0018.
Tension. § 6.1 normatively defines two recipe header shapes: explicit (recipe NAME [: deps]) and implicit (NAME : deps). The implicit form has zero usage in examples/ or standard/conformance/; every Cookfile in the project uses the explicit form. App. B.3.7 rationalises only the form’s column-0 anchor — the only published rationale is defensive (“here is how we keep it from causing trouble”). An accidental name: value at column 0 silently parses as a recipe named name with dep value, with no diagnostic.
Status quo. Both forms accepted, six parser tests pin the implicit form (three in cli/crates/cook-lang/src/tests.rs, three in cli/crates/cook-lang/src/lexer.rs), and App. A.3 lists both productions.
Possible resolution. Drop the implicit form. § 6.1 collapses to one shape; App. A.3’s recipe_header becomes "recipe" name (":" dependency_list)?; App. B.3.7 is deleted; six implicit-form parser tests are removed; one new negative conformance case under standard/conformance/negative/ asserts that name: deps at column 0 is rejected (becomes Token::Content and falls into “unexpected content outside of a recipe”). No corpus migration is required because the form is unused.
Composability. Removes the typo vector and lets a future E.5 (“drop end”) change rely on top-level keywords as the unambiguous block-boundary signal. The two are independent but compose well in either order; bundling them in a single cut is reasonable.
D.7. chore as a sibling of recipe
Status. Resolved in v0.4 by CS-0020.
Tension. Cook’s keyword surface conflates two operationally distinct things: build targets (cached, input-driven, output-producing) and command runners (uncached, sequence-of-shell-commands, no artifacts). Both are written as recipe NAME ... end, so a reader can’t tell from the header what a thing actually does. In this project’s own top-level Cookfile, only three of fifteen recipes use cook-step machinery (ts-generate, ts-wasm, nvim-install); the other twelve are pure command runners. The example projects show the same split: recipe play in examples/modules/fzf-picker/Cookfile is all-interactive task work; recipe clean in every example is rm -rf with no cook step. The conflation also hurts diagnostics — cook "out" using "..." inside a recipe-the-author-meant-as-a-task silently allocates a phantom build artifact and caches it on first run, with no signal that the author had something else in mind.
Status quo. Authors hand-mark interactivity per shell line via the @-prefix (§ 8.8) and rely on convention to distinguish task-shaped recipes from build-shaped ones. The engine treats both identically; caching is implicit on the presence of a cook step.
Possible resolution. Introduce a new top-level construct, chore NAME [: deps] ... end (or, post-E.5, chore NAME [: deps] ... <until-next-top-level>), as a thin specialization of recipe:
- Step kinds allowed:
shell_command,interactive_command,lua_line,lua_block,module_call. (Module call is debatable — modules typically emit cook steps; ban-from-chore is the conservative choice.) - Step kinds banned at parse time:
gather,cook_step,plate_step,test_step. The diagnostic names the keyword and points the author atrecipe. - Default-interactive: every
shell_commandinside achorebody is treated as if@-prefixed. The@marker remains valid (no-op) for symmetry with recipe bodies, but is unnecessary. - No caching. Chores always run when invoked. Engine-identical to a recipe with no outputs; the distinction is purely an authoring/static-checking layer over the existing engine.
- Deps: same
: dep1 dep2syntax. Chores may depend on recipes (run a build, post-process) and recipes may depend on chores (rare but legal).
The keyword choice is a deliberate departure from Cook’s culinary metaphor (recipe / gather / cook / plate / menu / serve). Plain English wins here — chore reads as “thing you just run” without the cognitive hop required for a strained metaphor (prep, serve).
Spec impact. New normative chapter on chores parallel to § 6, plus a chore_def production in App. A. App. B picks up a rationale section (why chore is a separate form rather than a flag on recipe, why the specific banned step kinds, why default-interactive). Conformance corpus gains ~3 positive cases (basic chore, chore with deps, chore with Lua block) and ~3 negative cases (chore with gather, with cook, with plate).
Migration. Purely additive — no existing Cookfile breaks. This project’s top-level Cookfile would convert ~12 recipes to chores in a single pass; example Cookfiles would convert their clean and similar recipes opportunistically; conformance fixtures would stay as recipes (they exercise recipe-specific semantics).
Composability. Stacks cleanly with E.5 (chores adopt the same implicit termination rule) and is orthogonal to E.6. Bundling all three into a single “recipe-form simplification” cut is reasonable; staging them across cuts is also fine.
D.8. Chore-body shell commands skip sigil-placeholder substitution
Status. Fully resolved in v0.7 by CS-0033 alongside the migration to $<TOKEN> sigil syntax.
Original symptom. § 9.3 normatively asserts that “every bare shell_command is a shell text” subject to the chapter’s placeholder substitution. Inside a recipe body, a plate shell-block body, or a cook { … } body the substitution fired correctly. Inside a chore body it did not — a {TOKEN} placeholder was emitted into the recorded unit’s command payload verbatim, and the unit failed at execute time when the shell tried to resolve a literal {TOKEN} as a command word.
Repro (historical). With cook-lang 0.1.0 (claiming Cook Standard v0.5):
config
env.ADB = "adb"
chore devices
$<ADB> devices -l
cook --emit-lua formerly emitted:
cook.recipe("devices", {}, function() cook._enter_chore() cook.add_unit({command = [[$<ADB> devices -l]], interactive = true, line = 4, cache = false}) cook._exit_chore()end)Resolution. The v0.7 sigil-placeholder change (CS-0033) replaced the {TOKEN} template layer with a strict $<IDENT> sigil scanner (§ 9.3) applied uniformly across all four shell-text contexts: cook { … } bodies, plate bodies, test bodies, and bare shell_command steps in both recipe and chore bodies. The chore-body codegen now applies the same sigil-expansion pass on every shell line, emitting cook.require_env("ADB") for $<ADB> rather than passing the literal through. Spec and implementation now agree.
Found in. Real-world Cookfile for the glovemap Android key-mapper project (~/dotfiles/android/glovemap/Cookfile), which exercises plate’s three iteration modes for adb-deploy and tripped on this when it tried to use $<ADB> and $<PACKAGE> env placeholders in chore bodies for devices / logs / uninstall.
D.9. No multi-line continuation for input lists
Resolved by CS-0078, then CS-0222 and CS-0229. The original concern — a long
input list has to fit on one physical line — was closed by CS-0078, which added
the STEP_CONTINUATION_NEWLINE token: successive quoted patterns and
!-prefixed excludes may sit on their own indented lines and still belong to
one declaration. That is live surface on gather, pinned by
standard/conformance/positive/058-multi-line-gather.
The list surface then moved. CS-0222 gave named determinant sets an indented
multi-line files NAME body, and CS-0229 removed ingredients; gather
remains solely the recipe iteration driver. Both halves are therefore closed
rather than deferred: continuation exists, and the list forms that motivated the
item are declared elsewhere.
D.10. {NAME} body-ref → wave-edge inference is not uniform across CLI commands
Status. Resolved in v0.5 by CS-0027. The single-Cookfile {NAME} body-ref-extraction loop was hoisted into compute_single_inferred_deps, the workspace counterpart was already in place, and both are invoked at every CLI entry that hands recipes off to cook_engine::run::run (cmd_run and the cmd_dag workspace branch added by CS-0027). The cmd_test runner that the original CS-0027 fix also covered was withdrawn in CS-0046; the runtime pins under examples/cmd_test_inferred_deps_e10/ and examples/cross_cookfile_test/ were deleted alongside it. The unit tests workspace_inferred_deps_* kept the helper under regression until COOK-423, which deleted the whole module: it had had no caller since the unified-DAG cut, and {NAME} body references are merged into requires by codegen instead; the single-Cookfile twin (and its single_inferred_deps_body_ref_produces_edge pin) was deleted when single-Cookfile projects became workspaces of one member, an equivalence pinned end-to-end by cli/crates/cook-cli/tests/workspace_of_one_equivalence.rs.
Residual scope (post-CS-0027). CS-0027 explicitly tracked two follow-on engine concerns out of scope. The cook.add_test propagation gap (“a test recipe whose body refs a sibling recipe will race that sibling under --jobs > 1”) was closed in CS-0030; the examples/cross_cookfile_test/ walkthrough that exercised it end-to-end was deleted by CS-0046 along with the rest of the cook --test runner surface. The wave-grouper bidirectional-BFS concern in build_inferred_groups (a reverse-inferred-edge recipe such as verify_X whose test body refs {X} is pulled into a cook X build) is still present in cli/crates/cook-engine/src/wave_grouper.rs (the BFS adds edges in both directions); it falls outside § 10.6‘s “A → B” wording and is tracked as a separate engine concern rather than a pre-1.0 Standard item.
Original symptom (historical, pre-CS-0027). § 10.6 normatively asserts that “a name reference to recipe B appearing in any step or dep-list of recipe A establishes a cross-recipe dependency from A to B.” The reference implementation realizes this contract through the inferred_deps map that the engine’s wave grouper consumes. The map was computed correctly in some CLI command paths and not at all in others, so a Cookfile that held the spec contract in one command (cook recipe) violated it in another (cook --test, since removed by CS-0046).
Concretely:
| Command | Single-Cookfile mode | Workspace mode |
|---|---|---|
cmd_run (cook NAME) | ✅ inferred_deps computed | ✅ inferred_deps computed |
cmd_dag (cook --dag) | ✅ inferred_deps computed | ❌ workspace mode unsupported |
cmd_test (cook --test) | ❌ passes &BTreeMap::new() | ❌ passes &BTreeMap::new() |
(cli/crates/cook-cli/src/pipeline.rs:731,764 for cmd_test; cmd_dag’s workspace branch is absent.)
Repro (under cmd_test). A Cookfile with two recipes — a prepare recipe with a cook step, and a verify recipe whose test body contains {prepare} — registers verify before prepare because the wave grouper sees no edge. cook.dep_output("prepare") (or string-substitution against a name-reference resolved in verify’s body) errors with “recipe ‘prepare’ has no terminal output (not registered or has no cook steps).” The same Cookfile invoked via cook prepare && cook verify works because cmd_run computes inferred_deps.
Status quo. Authors who hit this symptom under cook --test add an explicit recipe verify: prepare header, which the wave grouper picks up via RecipeInfo.requires regardless of which command path is in use. Same workaround as the original cmd_run workspace bug fixed in CS-NNNN — the pre-fix shape persists in cmd_test and in cmd_dag --workspace.
Workaround (unchanged). Use explicit : dep headers wherever a recipe consumes another recipe’s outputs. The body-ref-as-edge contract is reliable only under cmd_run.
Possible resolution. Hoist compute_workspace_inferred_deps and the equivalent single-Cookfile loop into a shared helper that every command path calls before invoking cook_engine::run::run. The helper already exists for cmd_run (pipeline.rs::compute_workspace_inferred_deps); extending cmd_test and cmd_dag to call it is a localized refactor. cmd_dag’s workspace branch is a separate gap (no workspace support at all) and is the larger of the two — adding workspace support to cmd_dag is a feature, not a bug fix, but bundling it with the inferred_deps unification is reasonable.
A single CS entry can close all three cases:
cmd_testworkspace mode: callcompute_workspace_inferred_deps.cmd_testsingle-Cookfile mode: call the same body-ref-extraction loopcmd_runuses.cmd_dagworkspace mode: add the workspace branch and callcompute_workspace_inferred_deps.
Conformance impact. A new positive fixture under standard/conformance/ that exercises a {NAME} body ref under each command path and asserts edge-respecting registration order, plus the existing single-Cookfile cmd_run conformance shape, would pin the contract uniformly.
D.11. CLI exit-code taxonomy
Status. Closed in v0.15 by CS-0124: the taxonomy graduated to the normative § 19.2, which is now the authoritative statement; the table below mirrors the reference implementation’s current mapping (introduced by CS-0041, updated by CS-0124). CS-0124 also fixed the runner-path regression this item’s wording had gone stale on: a failing test step body under cook RECIPE_NAME was reported as FAILED but exited 0; it now fails the run with exit 1 via TestFailure.
Symptom. The Cook Standard described language semantics but did not say what numeric exit code the cook CLI should produce on each failure mode. Users wiring cook into CI scripts had to read cli/crates/cook-cli/src/error.rs to know what to expect, and a regression that flipped an exit code (e.g., the v0.7-track cook --test runner returning 0 on test failure — see CS-0041; the runner itself was removed by CS-0046) had no spec hook to catch it.
Reference implementation taxonomy. The reference implementation (cli/crates/cook-cli/src/error.rs, CookError::exit_code) maps each error variant as follows:
| Exit code | Variant | Meaning |
|---|---|---|
0 | no error | All requested recipes built and any test bodies passed. |
1 | CommandFailed | A recipe step’s shell command exited non-zero. |
1 | TestFailure(n) | One or more executed test steps failed, timed out, or were blocked — emitted by both cook test and cook RECIPE as of CS-0124. |
1 | Other | Generic failure (I/O, codegen, registration, etc.). |
2 | ParseError | The Cookfile failed lexical or syntactic validation. |
3 | RecipeNotFound | The named recipe (or default build) is not declared in the Cookfile. |
3 | RecipeCollision | The named recipe is registered more than once (registration collision). |
The runtime pin lives in cli/e2e-fixtures/cli_audit_exit_codes/walkthrough.sh. The test-failure-specific runtime pin (examples/cli_audit_test_exit_code/walkthrough.sh) was deleted alongside the cook --test runner in CS-0046; as of CS-0124 a test step that fails, times out, or is blocked under cook RECIPE_NAME routes through TestFailure (exit 1) rather than CommandFailed, and that case is covered by cli/e2e-fixtures/cli_audit_exit_codes/.
Resolution. Pre-1.0, TestFailure deliberately shares exit status 1 with other execution failures (see Note 19.2.1 in § 19.2). Whether TestFailure should retain a distinct exit code (e.g., 4) so CI scripts can distinguish “build failed” from “tests failed” without scraping stderr moves to the v1.0 test-execution surface redesign (TBD); it is not resolved by CS-0124.
Workaround pre-1.0. Treat exit 1 as “something failed; see stderr for the variant tag.” The taxonomy is stable enough for CI conditioning on 0 vs non-zero; § 19.2 is the authoritative reference for the mapping.
D.12. events.jsonl wire format has no normative section
Status. Open as of v0.7. Recent fix CS-0043 corrected a wire-format honesty bug (stream field was hardcoded to "stdout" regardless of fd-of-origin) without normalizing the format itself.
Symptom. The events.jsonl JSON-lines stream emitted by cook (when the renderer is configured to JSON output) is the documented integration surface for downstream tooling — log aggregators, CI dashboards, the planned Cook Cloud build observability stack, and cook logs retrieval from ~/.cook/builds/. The format is currently defined only by the implementation: cli/crates/cook-progress/src/render/json.rs is the de-facto schema, with a small set of unit tests pinning the shape (build_started_uses_recipe_names, recipe_completed_uses_elapsed_ms_integer, node_output_uses_names_and_stream_string, each_event_is_one_line). No normative chapter or appendix in the Cook Standard describes the envelope (ts, v), the type discriminator, the per-event field schemas, or the enumerated value sets (stream: "stdout" | "stderr", reason: "upstream-failed" | "condition-false" | "disabled").
Why this matters pre-1.0. The Cook Standard versioning policy (CS-0012) treats wire formats produced by the reference implementation and consumed by external tooling as part of the language surface — what the Standard normatively describes is what downstream consumers can rely on across implementations and versions. Cutting v1.0 with events.jsonl defined only by cli/crates/cook-progress/src/render/json.rs would freeze the implementation as the contract, including any incidental shape choice (field ordering, null vs missing field, _ms integer vs string-with-units, etc.) that an alternative implementation might reasonably do differently.
Resolution path. Add a normative section (likely §9 “Wire formats” or under §8 “Execution model”) that covers:
- The envelope:
ts(RFC-3339 UTC),v(schema version integer), and the rule that future schema versions bumpv. - The
typediscriminator and the closed set of values:build-started,recipe-started,recipe-completed,recipe-failed,node-started,node-completed,node-failed,node-cache-hit,node-skipped,node-output,interactive-start,interactive-end,finished. - The per-event field schema for each
type, including required vs optional fields, value types, and enumerated value sets (stream: "stdout" | "stderr",reason: "upstream-failed" | "condition-false" | "disabled"). - The line-orientation invariant (one event per
\n-terminated line, no embedded newlines in field values without escaping). - The “names not opaque IDs” rule: every reference to a recipe or node uses the human-readable name, never the internal
RecipeId/NodeId(a property currently enforced by thename_lookuphelpers injson.rs).
A companion conformance harness — JSON-lines validators against fixture builds — pins the format at corpus level so a future implementation’s renderer can be checked against the same contract as the reference cook-cli.
Found in. Surfaced during the CS-0043 fix: the bug was traceable only because the unit test in cli/crates/cook-progress/src/render/json.rs asserted "stream":"stderr" while the upstream pipeline made the Stream::Stderr variant unreachable. Without the test the wire-format lie would have been silent. With a normative section, the same audit could have been performed against the spec rather than against the test.
D.13. $<NAME> cross-recipe substitution does not lower in plate shell bodies
Status. Open as of v0.8. Cross-references E.14 (the Lua-body analogue).
Symptom. § 10.5 normatively asserts that a $<NAME> reference in a cook using shell block, a plate shell block, a test shell block, or a bare shell_command MUST be substituted by the space-joined concatenation of the named recipe’s output list. The substitution fires correctly inside cook { … } bodies but does NOT fire inside plate { … } shell bodies in cook 0.8 — the literal $<NAME> flows through to the recorded unit’s command payload and the shell errors on it at execute time.
Repro. With cook-lang 0.1.0 (claiming Cook Standard v0.8):
recipe debug-apk
gather "src/main.kt"
cook "build/app.apk" { touch $<out> }
recipe install: debug-apk
plate {
adb install -r $<debug-apk>
}
cook emit-lua for install:
cook.recipe("install", {requires = {"debug-apk"}}, function() cook.step_group(function() cook.add_unit({command = "set -e\nadb install -r $<debug-apk>", cache = false, consulted_env_keys = {}}) end)end)The requires edge is correctly inferred (good), but the command string carries $<debug-apk> verbatim. At execute time the shell parses $<debug-apk> as $ followed by <debug-apk>, which is a redirection error: /bin/sh: -c: line N: syntax error near unexpected token 'newline'.
The same $<NAME> reference inside a cook ... { … } body of the same recipe lowers correctly: emitted Lua becomes ... " install -r " .. cook.dep_output("NAME"). The asymmetry sits between the two shell-body surfaces, not in the lexer or the § 10 resolution surface.
Status quo. Plate shell bodies are codegen’d by cli/crates/cook-luagen/src/recipe.rs. The cook-step shell-body path scans for $<NAME> tokens and emits cook.dep_output(NAME) interpolations; the plate-step shell-body path does not. The § 10.6 edge-inference path does fire for plate bodies (the requires = {"debug-apk"} in the repro above confirms this), so the recipe sees the dep and orders correctly — the gap is in § 10.5 lowering only.
Workaround. Hardcode the dependent recipe’s output path inside the plate body, leaving the : NAME dep edge for ordering:
recipe install: debug-apk
plate {
adb install -r build/app.apk
}
This couples the consumer to the producer’s output filename, which the $<NAME> form was meant to abstract.
Resolution path. Extend cook-luagen’s plate-shell-body codegen to apply the same $<NAME> lowering pass that fires for cook { … } shell bodies. The lowering rule is identical (§ 10.5, “Lua lowering” note): $<NAME> → cook.dep_output("NAME"), $<NAME.ACCESSOR> → cook.dep_output_list("NAME")-driven form. Add a positive conformance fixture under standard/conformance/positive/ exercising plate { $<NAME> } with a non-empty output list, and a runtime fixture under examples/ that asserts the substitution at execute time. The § 10.6 edge inference already works for the same surface, so the spec/impl gap is one-directional.
Found in. Real-world Cookfile for the glovemap Android key-mapper project (~/dev/glovemap/Cookfile). The project’s install recipe consumes the APK produced by the debug-apk recipe and ships it to a tablet via adb; the natural shape is adb install -r $<debug-apk> inside a plate { … } block. Hardcoded path workaround in production.
D.14. cook.dep_output is not installed on execute-phase worker VMs
Status. Open as of v0.8. Cross-references E.13 (the shell-body analogue).
Symptom. § 10.5 states: “A $<NAME> reference inside a using_lua_block is parsed as Lua syntax (a single-element table containing the local variable NAME); cross-recipe references in Lua bodies use cook.dep_output(NAME) and cook.dep_output_list(NAME).” § 23.2 confirms the same for plate and test Lua bodies: “Cross-recipe references inside a plate or test Lua body use cook.dep_output() / cook.dep_output_list(), identical to a >{ … } Lua body.”
Plate and test Lua bodies run at execute phase (§ 13.1). The implementation registers cook.dep_output and cook.dep_output_list only on the register-phase Lua VM (cli/crates/cook-register/src/dep_output_api.rs, register_dep_output_api); the execute-phase worker pool (cli/crates/cook-luaotp/src/pool.rs) installs cook.env, inputs, outputs, input, output, input_N, but not dep_output / dep_output_list. A plate >{ ... } body that calls cook.dep_output("foo") therefore errors at execute time with attempt to call a nil value (field 'dep_output').
Repro. With cook-lang 0.1.0:
recipe upstream
cook "build/u.txt" { echo hi > $<out> }
recipe downstream: upstream
plate >{
local path = cook.dep_output("upstream")
cook.sh("cat " .. path)
}
Output of cook downstream:
[downstream/$lua] lua error: runtime error: ...attempt to call a nil value (field 'dep_output') [C]: in field 'dep_output' [string "..."]:1: in main chunkThe same call from a >{ … } body (which runs at execute phase on a worker VM, per the spec) would presumably also fail; the surface-level wording in § 23.1 implies the API is available there too. The two surfaces share the worker-VM context, so the gap is identical.
Status quo. Cross-recipe dep refs from Lua bodies have no working surface at execute time. Authors of plate Lua bodies cannot reach a dependency’s output path through the spec’d API. The only path that works today is the shell-body form (§5.5 surface form), which itself has the partial gap documented in E.13 — together, the two entries cover every surface a Cookfile author would reach for to express “this plate operates on a dep’s output.”
Workaround. Use a shell plate body and hardcode the dep’s output path:
recipe downstream: upstream
plate { cat build/u.txt }
Or, if the dep’s output path is parametric (e.g., a globbed multi-output cook step), restructure so the consumer has its own gather line that re-globs the producer’s output directory — accepting that the glob resolves at register time and may be empty on first build.
Resolution path. Two options:
- Install
cook.dep_output/cook.dep_output_liston the worker VM. The register-phase implementation incook-register/src/dep_output_api.rsreads fromSharedTerminalOutputs, which is a runtime structure populated bycook.add_unitcalls. The same structure is plumbable to worker VMs throughpool.rs::set_globals_on_vm(wherecook.env,inputs, etc. are installed). Each worker VM gets a read-only reference to the terminal-outputs table at unit-execution time. This option resolves E.14 directly and is the surface-faithful fix. - Lower
cook.dep_output(N)calls in plate/test Lua bodies at register phase. Treat plate/test Lua bodies the same way cook-step>{ … }bodies are lowered today (per the § 10.5 “Lua lowering” note): scan the body forcook.dep_output(N)/cook.dep_output_list(N)calls and emit register-phase substitutions whose results are baked into thelua_codepayload. This option avoids changes to the worker VM at the cost of a more invasive codegen pass and a register-time eager-evaluation semantics shift (the dep’s output must be known at the register-phase moment of the consumingadd_unitcall).
Option 1 is preferred — it matches the spec’s surface semantics (the API call happens at execute time) and is a local change to the worker pool. Option 2 is the “treat plate Lua bodies like cook-step Lua bodies” path and is the surface-most-compatible approach if option 1 turns out to have unforeseen interactions with the execute-phase scheduler.
A positive conformance fixture under standard/conformance/positive/ exercising plate >{ cook.dep_output("X") } plus a runtime pin under examples/ asserts the chosen resolution.
Found in. Same glovemap project as E.13. The project’s first cut of recipe install was a Lua plate calling android.adb_install(cook.dep_output("debug-apk"), ...). After hitting this gap, the recipe was rewritten as a shell plate with a hardcoded APK path (which then surfaced E.13).
D.15. Bare env.X = "..." in config clobbers --set X=...
Status. RESOLVED in v0.18 by CS-0172 — via this item’s option 3 (reverse the precedence), not the option 1 it recommended. --set is applied after both config bodies run, so it wins however the block was authored and the or-idiom is no longer load-bearing. CS-0172 also rejects a --set naming a variable no config block declared, which was the other half of the trap: the flag silently invented variables. The record below is retained for the reasoning.
Symptom. The --set KEY=VALUE CLI flag is the documented mechanism for overriding env defaults at invocation (cook --help output: “Override a variable (KEY=VALUE), repeatable”). A user reflexively writes env.X = "default" inside a config block to declare a default, expecting --set X=other to take precedence at runtime. It does not: the config block executes after --set populates cook.env, so the bare assignment overwrites the CLI-supplied value with the literal. The override silently fails.
Status quo. §{lua.env-alias} states only that env and cook.env are bound to the same table inside a config_block body; the Standard does not specify how the table is populated relative to CLI overrides, nor what an unqualified assignment means. In practice, --set is applied to cook.env before any config block runs, and config-block assignments run as ordinary Lua against that table. Last write wins: a bare env.X = "..." is the last write.
Repro. With cook-lang 0.1.0:
config
env.DEVICE = ""
chore show
> print("DEVICE = " .. cook.env.DEVICE)
$ cook --set DEVICE=tablet:5555 showDEVICE =The user expected tablet:5555. The config block’s env.DEVICE = "" ran after --set populated the env, clobbering the override.
Workaround. Use the or-default idiom on every line of a config block:
config
env.DEVICE = env.DEVICE or ""
This preserves an existing value (from --set) and falls back to the literal only when the key is unset. It works, but every author writing a config block has to remember the idiom; missing the or is a silent bug — the override mechanism advertised in --help simply doesn’t take effect, with no diagnostic.
Resolution path. Three options, in order of preference:
-
Add an explicit defaults-helper. Introduce
cook.default(key, value)(orenv.default("X", "...")) as the canonical surface for declaring a config default. The helper is a no-op when the key is already set (from--set, from animport-passed config, etc.) and assigns the literal otherwise. The bareenv.X = "..."form retains its current “last-write-wins” Lua semantics — the helper is the documented idiom and the Standard cookbook prescribes it. Migration is purely additive; existing Cookfiles continue to work, and theor-idiom remains valid but no longer load-bearing. Spec impact: one new §6 subsection naming the helper, one positive conformance fixture, an example or two updated. -
Diagnostic at register phase. When a config-block assignment overwrites a
--set-supplied value with a different literal, emit a--set X=A clobbered by config line N (env.X = "B")warning at register time. Authors who actually want to override the CLI from inside a config block (rare) can silence the warning with theoridiom. This option avoids any language change; it adds a friction-point that future authors will hit on their first run and be able to fix locally. Spec impact: one §8 paragraph on the diagnostic, optional negative conformance case. -
Reverse the precedence. Make
--setapply after config blocks run, so the CLI flag truly wins regardless of how the config block was authored. This is the most behaviorally surprising option (Lua last-write-wins is broken for the env table specifically) but most user-intuition-friendly. Spec impact: a §8 normative rule on the ordering, and a migration note explaining why some existing Cookfiles that depended on config-block-overriding-CLI behavior change. Probably not the right call — option 1 is cleaner.
Option 1 is preferred. It documents the right shape, doesn’t break Lua semantics, and gives authors a one-line idiom that does exactly what they meant.
Found in. Same glovemap project as E.13/E.14. The install recipe was failing to push to the right device because cook --set DEVICE=tablet:5555 install was being clobbered by env.DEVICE = "" in the config block. The fix was to convert every config-block line to the env.X = env.X or "..." idiom; the realization that --set had been silently ignored took several minutes of debugging an adb “more than one device/emulator” error that pointed elsewhere.