Allow rules to execute ordered command lists (#550) - #554
Conversation
Widen the manifest command schema from a single string to a scalar or a non-empty ordered list. Each list entry is Jinja-rendered and interpolated independently, then emitted as a single fail-fast '&&' shell chain so the build stops at the first non-zero exit. An empty command list is rejected during deserialization with a localized diagnostic. The scalar form serializes byte-identically, so existing action hashes and snapshots stay unchanged. Reuse StringOrList for the field and add From impls so existing construction sites keep compiling. Co-Authored-By: Claude <noreply@anthropic.com>
The users' guide now describes the scalar-or-list command recipe, its declaration-order and fail-fast shell-chain semantics, the shared-shell state caveat, and a documented example, alongside when to prefer 'script'. The design doc records the same schema and shell semantics, and the changelog notes the new manifest form. Co-Authored-By: Claude <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Summary
WalkthroughChangesSupport scalar commands and ordered, non-empty command lists. Interpolate each list entry independently. Render lists as one fail-fast Ordered command list support
Sequence Diagram(s)sequenceDiagram
participant ManifestParser
participant CommandRenderer
participant IRConversion
participant NinjaGenerator
participant ProcessRunner
ManifestParser->>CommandRenderer: provide scalar or ordered command list
CommandRenderer->>IRConversion: render and interpolate each entry
IRConversion->>NinjaGenerator: provide interpolated recipe
NinjaGenerator->>NinjaGenerator: emit brace groups joined with &&
NinjaGenerator->>ProcessRunner: execute generated Ninja command
ProcessRunner->>ProcessRunner: capture action and entry failure marker
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (5 warnings, 4 inconclusive)
✅ Passed checks (11 passed)
📋 Issue PlannerBuilt with CodeRabbit's Coding Plans for faster development and fewer bugs. View plan used: ✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Reviewer's GuideExtend Flow diagram for command list processing from manifest to Ninjaflowchart LR
ManifestCommand[StringOrList command in manifest]
Render[render_recipe_string_or_list]
IR[register_action interpolate_command]
Ninja[ninja_gen write_recipe join with &&]
ManifestCommand --> Render
Render --> IR
IR --> Ninja
subgraph StringOrListVariants
StringVariant[String]
ListVariant[List]
EmptyVariant[Empty]
end
ManifestCommand --> StringVariant
ManifestCommand --> ListVariant
ManifestCommand --> EmptyVariant
ListVariant --> Render
ListVariant --> IR
ListVariant --> Ninja
StringVariant --> Render
StringVariant --> IR
StringVariant --> Ninja
EmptyVariant --> ManifestError[manifest.command_list_empty diagnostic]
EmptyVariant --> NinjaGuard[reject_empty_command_recipe in debug]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae55b27f3f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Concatenating entries with '&&' alone let a later entry's own '||', ';' or '&' escape the entry boundary and mask an earlier failure: entries 'false' and 'false || echo recovered' became 'false && false || echo recovered', which POSIX evaluates as (false && false) || echo, reporting success after the first entry failed. Wrap each entry in a brace group so it forms a distinct shell unit before the fail-fast '&&' between entries. Braces run in the current shell (unlike '( ... )'), so working directory, environment, and variables set by one entry still carry into the next, keeping the documented shared-shell-state semantics. Add integration tests for the masking scenario and for environment state carrying across entries. Co-Authored-By: Claude <noreply@anthropic.com>
Translate the manifest.command_list_empty diagnostic into all 33 non-English catalogues, following each locale's quotation conventions; only the en-GB and en-US catalogues keep the English source text. Name the failing list position when a command list entry fails to render, so a Jinja error identifies the entry rather than only the recipe stage. Drop the debug-only panic and its two clippy expectations from reject_empty_command_recipe; Display::to_string already escalates the returned fmt::Error, so the fault still surfaces loudly. Compare scalar command assertions against StringOrList::String directly, since as_single also accepts a single-element list and so does not prove the scalar variant was preserved. Correct the users' guide and design doc: a command list is fail-fast, so a later entry runs only when the preceding entry exits zero. Only the working directory, environment, and shell variables carry forward, and a failed entry may leave side effects behind. Reflow the shell-quote link paragraph within 80 columns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Around line 26-27: Update the changelog sentence near the shell-chain
description by inserting a comma before “so” to separate the descriptive clause
from the result clause.
In `@docs/netsuke-design.md`:
- Line 719: Synchronize all command schema documentation with StringOrList: in
docs/netsuke-design.md lines 719-719, change the Mermaid RECIPE.command field
from string to StringOrList; in docs/netsuke-design.md lines 253-257, describe
scalar pass-through and list lowering into brace groups joined with &&, removing
the verbatim claim; in src/ast.rs lines 145-149, update the Recipe::Command
Rustdoc with the same scalar and list behavior.
In `@locales/ar/messages.ftl`:
- Line 152: Update the manifest.command_list_empty translation to specifically
state that the command list must not be empty, while still indicating that a
command string is an accepted alternative; do not imply that the scalar value
command: "" is rejected.
In `@src/ninja_gen.rs`:
- Around line 218-230: Update the StringOrList::List serialization in the
command_line construction to use a shell-safe boundary that remains valid when
an entry contains an inline comment or ends with &, while preserving brace-group
isolation and the fail-fast && chain. Add regression tests covering both
inline-comment entries and entries ending with &.
In `@tests/ninja_snapshot_tests.rs`:
- Around line 135-136: Update the fixture-loading code in the ninja snapshot
test to read multi_command.yml through a cap_std::fs_utf8::Dir capability
instead of std::fs::read_to_string, preserving the existing context error
handling and fixture path.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8310c81e-5564-4147-9a10-e07b9a86c415
⛔ Files ignored due to path filters (1)
tests/snapshots/ninja/ninja_snapshot_tests__multi_command_manifest_ninja.snapis excluded by!**/*.snap
📒 Files selected for processing (62)
CHANGELOG.mddocs/netsuke-design.mddocs/users-guide.mdlocales/ar/messages.ftllocales/cs/messages.ftllocales/cy/messages.ftllocales/da/messages.ftllocales/de/messages.ftllocales/el/messages.ftllocales/en-GB/messages.ftllocales/en-US/messages.ftllocales/es-419/messages.ftllocales/es-ES/messages.ftllocales/fa/messages.ftllocales/fi/messages.ftllocales/fr/messages.ftllocales/gd/messages.ftllocales/he/messages.ftllocales/hi/messages.ftllocales/hu/messages.ftllocales/id/messages.ftllocales/it/messages.ftllocales/ja/messages.ftllocales/ko/messages.ftllocales/nb/messages.ftllocales/nl/messages.ftllocales/pl/messages.ftllocales/pt-BR/messages.ftllocales/pt-PT/messages.ftllocales/ro/messages.ftllocales/ru/messages.ftllocales/sv/messages.ftllocales/th/messages.ftllocales/tr/messages.ftllocales/uk/messages.ftllocales/vi/messages.ftllocales/zh-Hans/messages.ftllocales/zh-Hant/messages.ftlsrc/ast.rssrc/ir/from_manifest_support.rssrc/localization/keys.rssrc/manifest/mod.rssrc/manifest/render.rssrc/manifest/tests/workspace.rssrc/ninja_gen.rssrc/ninja_gen_tests.rstests/ast_tests.rstests/ast_tests/parsing.rstests/ast_tests/recipe.rstests/ast_tests/string_or_list.rstests/bdd/steps/manifest/mod.rstests/bdd/steps/manifest/targets.rstests/command_escaping_tests.rstests/data/multi_command.ymltests/documentation_examples_tests.rstests/hasher_tests.rstests/ir_from_manifest_tests.rstests/ir_tests.rstests/manifest_env_tests.rstests/manifest_jinja_tests.rstests/ninja_gen_integration_tests.rstests/ninja_snapshot_tests.rs
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
leynos/rstest-bdd(auto-detected)leynos/ortho-config(auto-detected)leynos/whitaker(auto-detected)leynos/shared-actions(auto-detected)
|
@coderabbitai review |
|
The .vtcode/ directory holds transient session tool-output logs and vtcode.toml is a machine-specific agent configuration referencing a local API key environment variable. Neither belongs in the repository; follow the existing convention that already ignores .claude/, .crush/, .grepai/, and .memdb/.
Evaluate list entries as safely quoted shell text within their brace groups. This prevents inline comments and trailing background operators from swallowing the chain delimiter while preserving order, fail-fast behaviour, and shared shell state. Align the related schema documentation, Arabic diagnostic, fixture capability access, and Ninja snapshots.
|
@coderabbitai Have the following failed checks now been resolved? If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures. Do not treat warnings as optional or aspirational. Where a change is out of scope for this PR, propose a GitHub issue unless one exists already. (Treat o11y, code safety, documentation and validation coverage as in scope). ❌ Failed checks (2 errors, 5 warnings)
|
This comment was marked as resolved.
This comment was marked as resolved.
Cover direct target command lists from rendering through real Ninja execution, and reject programmatic empty command recipes explicitly. Isolate and attribute list-entry failures without exposing command payloads, while preserving the byte-identical scalar output path. Reuse bindings per recipe and document the lowering contract.
There was a problem hiding this comment.
Gates Failed
Enforce advisory code health rules
(1 file with Code Duplication)
Our agent can fix these. Install it.
Gates Passed
5 Quality Gates Passed
Reason for failure
| Enforce advisory code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| command_env_ui_tests.rs | 1 advisory rule | 10.00 → 9.39 | Suppress |
Quality Gate Profile: Pay Down Tech Debt
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
| fn command_list_public_api_fixture_compiles() -> io::Result<()> { | ||
| let rlib = NetsukeRlib::build()?; | ||
| let output = rlib.compile("tests/ui/command_list_public_api_pass.rs")?; | ||
|
|
||
| if !output.status.success() { | ||
| return Err(io::Error::other(format!( | ||
| "the command-list public API fixture should compile:\n{}", | ||
| stderr(&output), | ||
| ))); | ||
| } | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
❌ New issue: Code Duplication
The module contains 2 functions with similar structure: command_env_embedder_fixture_compiles,command_list_public_api_fixture_compiles
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/ninja_gen.rs`:
- Around line 352-358: Update command_list_entry and the list-entry execution
flow so exit commands are handled before the eval-based failure wrapper can
bypass marker emission. Add a runtime regression covering a first entry of exit
23, asserting the next entry is not executed, the overall status is 23, and the
failure marker identifies entry 1.
- Around line 241-247: Update validate_action_recipe in src/ninja_gen.rs:241-247
to use command.is_empty_content() so programmatic empty command lists return
EmptyCommandRecipe instead of emitting an empty command. Add regression cases
covering empty command lists in src/ninja_gen_property_tests.rs:182-199 and
src/ninja_gen_tests.rs:135-155.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1824da0e-ce56-4b69-ae99-efb8690cb694
⛔ Files ignored due to path filters (1)
tests/snapshots/ninja/ninja_snapshot_tests__multi_command_manifest_ninja.snapis excluded by!**/*.snap
📒 Files selected for processing (26)
.gitignoreCHANGELOG.mddocs/developers-guide.mddocs/netsuke-design.mddocs/users-guide.mdlocales/ar/messages.ftlsrc/ast.rssrc/ir/cmd_interpolate.rssrc/ir/from_manifest_support.rssrc/ir/from_manifest_support_tests.rssrc/manifest/render.rssrc/manifest/render_command_list_tests.rssrc/ninja_gen.rssrc/ninja_gen_property_tests.rssrc/ninja_gen_tests.rssrc/runner/process/child_exit.rssrc/runner/process/failure_attribution.rssrc/runner/process/mod.rssrc/runner/process/tests.rstests/command_env_ui_tests.rstests/documentation_examples_tests.rstests/logging_stderr/command_list_failure.rstests/logging_stderr_tests.rstests/ninja_gen_command_list_integration_tests.rstests/ninja_snapshot_tests.rstests/ui/command_list_public_api_pass.rs
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
leynos/rstest-bdd(auto-detected)leynos/ortho-config(auto-detected)leynos/whitaker(auto-detected)leynos/shared-actions(auto-detected)
| if matches!( | ||
| action.recipe, | ||
| Recipe::Command { | ||
| command: StringOrList::Empty | ||
| } | ||
| ) { | ||
| return Err(NinjaGenError::EmptyCommandRecipe { action_index }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '237,250p' src/ninja_gen.rs
sed -n '182,199p' src/ninja_gen_property_tests.rs
sed -n '135,155p' src/ninja_gen_tests.rs
rg -n -C 3 'is_empty_content|StringOrList::List\(Vec::new\(\)\)' src testsRepository: leynos/netsuke
Length of output: 4248
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- StringOrList implementation ---'
sed -n '330,390p' src/ast.rs
printf '%s\n' '--- generation and recipe emission ---'
rg -n -C 6 'validate_action_recipe|Recipe::Command|command =' src/ninja_gen.rs src/ast.rs
printf '%s\n' '--- relevant tests and declarations ---'
rg -n -C 4 'EmptyCommandRecipe|StringOrList' src/ninja_gen.rs src/ninja_gen_tests.rs src/ninja_gen_property_tests.rs tests/ast_tests/string_or_list.rsRepository: leynos/netsuke
Length of output: 25601
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
ast = Path("src/ast.rs").read_text()
method = re.search(
r'pub const fn is_empty_content\(&self\) -> bool \{(?P<body>.*?)\n \}',
ast,
re.S,
)
assert method, "is_empty_content implementation not found"
body = method.group("body")
assert "Self::Empty => true" in body
assert "Self::String(_) => false" in body
assert "Self::List(values) => values.is_empty()" in body
generator = Path("src/ninja_gen.rs").read_text()
validation = re.search(
r'const fn validate_action_recipe\(.*?\n\}',
generator,
re.S,
)
assert validation, "validate_action_recipe implementation not found"
assert "StringOrList::Empty" in validation.group(0)
assert "is_empty_content" not in validation.group(0)
print("StringOrList::List(Vec::new()) is classified as empty content.")
print("validate_action_recipe checks only StringOrList::Empty, so an empty list bypasses validation.")
PYRepository: leynos/netsuke
Length of output: 241
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '260,306p' src/ninja_gen.rs
python3 - <<'PY'
from pathlib import Path
import re
ast = Path("src/ast.rs").read_text()
method = re.search(
r'pub const fn is_empty_content\(&self\) -> bool \{(?P<body>.*?)\n \}',
ast,
re.S,
)
assert method, "is_empty_content implementation not found"
body = method.group("body")
assert "Self::Empty => true" in body
assert "Self::String(_) => false" in body
assert re.search(r"Self::List\(\w+\) => \w+\.is_empty\(\)", body)
generator = Path("src/ninja_gen.rs").read_text()
validation = re.search(
r'const fn validate_action_recipe\(.*?\n\}',
generator,
re.S,
)
assert validation, "validate_action_recipe implementation not found"
validation_body = validation.group(0)
assert "StringOrList::Empty" in validation_body
assert "is_empty_content" not in validation_body
list_branch = re.search(
r'Recipe::Command \{\s*command: StringOrList::List\(items\),(?P<body>.*?)\n\s*}\s*Recipe::Command',
generator,
re.S,
)
assert list_branch, "command-list branch not found"
assert '.join(" && ")' in list_branch.group("body")
assert 'writeln!(f, " command = {command_line}")' in list_branch.group("body")
print("StringOrList::List(Vec::new()) is classified as empty content.")
print("validate_action_recipe checks only StringOrList::Empty.")
print("The command-list branch joins zero items to an empty command_line and writes it as a Ninja command.")
PYRepository: leynos/netsuke
Length of output: 2615
Reject programmatic empty command lists.
Use command.is_empty_content() in validate_action_recipe. An empty list currently bypasses validation, joins to an empty command, and emits command =.
Add empty-list regression cases to both generator test sites.
📍 Affects 3 files
src/ninja_gen.rs#L241-L247(this comment)src/ninja_gen_property_tests.rs#L182-L199src/ninja_gen_tests.rs#L135-L155
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/ninja_gen.rs` around lines 241 - 247, Update validate_action_recipe in
src/ninja_gen.rs:241-247 to use command.is_empty_content() so programmatic empty
command lists return EmptyCommandRecipe instead of emitting an empty command.
Add regression cases covering empty command lists in
src/ninja_gen_property_tests.rs:182-199 and src/ninja_gen_tests.rs:135-155.
| fn command_list_entry(command: &str, action_index: usize, entry_index: usize) -> String { | ||
| let context = format!("{COMMAND_LIST_FAILURE_PREFIX}{action_index}, entry {entry_index}"); | ||
| format!( | ||
| "{{ if eval {}; then :; else _netsuke_command_status=$$?; printf '%s\\n' '{}' >&2; exit \"$$_netsuke_command_status\"; fi; }}", | ||
| shell_single_quote(command), | ||
| context, | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
set +e
output=$(
/bin/sh -c \
'{ if eval '"'"'exit 23'"'"'; then :; else status=$?; printf "%s\n" "marker"; exit "$status"; fi; }' \
2>&1
)
status=$?
set -e
test "$status" -eq 23
test "$output" != "marker"Repository: leynos/netsuke
Length of output: 152
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- relevant definitions and call sites ---'
rg -n -A18 -B12 'fn command_list_entry|validate_action_recipe|command_list_entry|StringOrList|COMMAND_LIST_FAILURE_PREFIX' src tests || true
printf '%s\n' '--- module outline ---'
ast-grep outline src/ninja_gen.rs
printf '%s\n' '--- focused source ---'
sed -n '210,270p' src/ninja_gen.rs
sed -n '330,370p' src/ninja_gen.rsRepository: leynos/netsuke
Length of output: 50373
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- command-list integration tests ---'
sed -n '185,285p' tests/ninja_gen_integration_tests.rs
printf '%s\n' '--- all command-list test names ---'
rg -n 'command_list|failure attribution|COMMAND_LIST_FAILURE_PREFIX|netsuke command-list failure' tests src --glob '*.rs'
printf '%s\n' '--- StringOrList definition and emptiness helpers ---'
rg -n -A80 -B15 'enum StringOrList|is_empty_content|impl.*StringOrList' srcRepository: leynos/netsuke
Length of output: 21919
🏁 Script executed:
set -euo pipefail
python3 - <<'PY'
import subprocess
command = r'''{ if eval 'exit 23'; then :; else _netsuke_command_status=$?; printf '%s\n' 'netsuke command-list failure: action 1, entry 1' >&2; exit "$_netsuke_command_status"; fi; } && { if eval 'printf second-entry-ran >&2'; then :; else _netsuke_command_status=$?; printf '%s\n' 'netsuke command-list failure: action 1, entry 2' >&2; exit "$_netsuke_command_status"; fi; }'''
p = subprocess.run(['/bin/sh', '-c', command], text=True, capture_output=True)
print(f"status={p.returncode}")
print(f"stdout={p.stdout!r}")
print(f"stderr={p.stderr!r}")
PYRepository: leynos/netsuke
Length of output: 182
Handle exit commands before failure attribution is bypassed.
When a list entry executes exit, eval terminates the current shell before the failure marker is printed. Add a runtime regression for a first entry of exit 23. Verify that the next entry does not run, the status remains 23, and the marker identifies entry 1.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/ninja_gen.rs` around lines 352 - 358, Update command_list_entry and the
list-entry execution flow so exit commands are handled before the eval-based
failure wrapper can bypass marker emission. Add a runtime regression covering a
first entry of exit 23, asserting the next entry is not executed, the overall
status is 23, and the failure marker identifies entry 1.
Summary
Allow a rule's
commandfield to accept either the existing scalar string ora non-empty ordered list of command strings. A command list runs its entries
in declaration order and stops at the first non-zero exit, so a reusable rule
can compose several distinct commands without a hand-written shell chain, a
script block, or a nested Netsuke invocation.
Closes #550
Manifest shape
A scalar command is unchanged:
A list is now accepted too:
Semantics
{{ ins }}/{{ outs }}interpolation per entry during IR lowering.
exit-code state carry forward like a
scriptblock.localized diagnostic.
remain byte-identical.
Implementation
Recipe::Commandnow holds aStringOrList;From<&str>,From<String>,and
From<Vec<String>>keep existing construction sites compiling.render_recipe_string_or_listrenders each list entry with theins/outsplaceholder injection.scalar-vs-list shape.
&&into a single fail-fast chain.Tests
Parsing, rendering, IR interpolation, and Ninja generation are covered for
both forms, plus ordering, fail-fast behaviour, Jinja rendering, empty-list
rejection, and a new
multi_command.ymlfixture with a Ninja snapshot. Theusers' guide and design doc document command lists.
References
Generated with Claude Code
Summary by Sourcery
Allow command recipes for rules and targets to be specified as either a scalar string or a non-empty ordered list, executed as a single fail-fast shell chain and rejected if empty.
New Features:
commandrecipes alongside the existing scalar command form, with each entry independently interpolated for inputs and outputs.commandlist is empty instead of silently accepting it.Enhancements:
commandrecipes to Ninja as a single&&-joined fail-fast chain while preserving existing scalar command behaviour and hashing.StringOrListAST helper with conversions, emptiness checks, and utility accessors used across manifest parsing, IR generation, and Ninja output.Documentation:
commandlist syntax, execution semantics, and usage guidance in the users' guide and design document, including a tested example manifest.Tests: