Skip to content

Escape backend dollar syntax after Netsuke placeholder lowering (3.14.7) - #565

Draft
leynos wants to merge 3 commits into
mainfrom
3-14-7-escape-backend-dollar-syntax-after-netsuke-placeholder-lowering
Draft

Escape backend dollar syntax after Netsuke placeholder lowering (3.14.7)#565
leynos wants to merge 3 commits into
mainfrom
3-14-7-escape-backend-dollar-syntax-after-netsuke-placeholder-lowering

Conversation

@leynos

@leynos leynos commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

Drafts the execution plan for roadmap task 3.14.7, which makes the Ninja backend escape residual literal dollars as $$ after Netsuke's own placeholder lowering, so shell variables such as $PATH, ${CARGO:-cargo}, and $RUSTFLAGS survive to the shell while the intermediate representation stays free of Ninja-specific escaping.

Plan: docs/execplans/3-14-7-escape-backend-dollar-syntax-after-netsuke-placeholder-lowering.md

This is a plan only. No production code changes. The plan requires approval before implementation begins.

What reconnaissance found

Reconnaissance and an adversarial design review established several facts that reshape the task beyond its roadmap wording. All were reproduced against ninja 1.11.1.

  • Two distinct failures exist today, not one. command = echo PATH is $PATH is silently erased by Ninja's lexer and prints PATH is . ${CARGO:-cargo} is a hard parse errorbad $-escape (literal $ must be written as $$). A test matrix built only on "the variable came back empty" would miss the case the roadmap names explicitly.
  • $in and $out already work inside script: recipes by accident, via Ninja's own built-in variables, because register_action (src/ir/from_manifest_support.rs:54) lowers only command recipes and passes scripts through unchanged. Escaping alone would regress every such script, so script lowering must land before escaping.
  • The script wrapper is corrupted today, not merely the variable. printf %b 'echo HOME is \$HOME' executes as printf %b 'echo HOME is \' — Ninja eats $HOME and leaves a trailing backslash inside the quoted argument.
  • A scalar command: containing a newline injects raw Ninja syntax into the generated file, creating targets the manifest never declared. shlex::split treats \n as whitespace, so neither the IR validation nor the assert_shell_command debug guard rejects it. Command lists are guarded; scalar commands are not.
  • Escaping commands while leaving path emission raw would make the dependency edge and the command disagree for a path such as input$1 — which tests/command_escaping_tests.rs:55 already uses as a fixture.
  • The existing snapshot corpus is blind here. Only one Ninja snapshot contains a $ at all, and it comes from the command-list wrapper rather than user text. The proptest generator at src/ninja_gen_property_tests.rs:177 is echo [a-z]{1,12}, which cannot produce a $.

Proposed approach

A ShellTextNinjaValue seam in a new src/ninja_gen_escape.rs, with private fields and a single fallible constructor, so "escaped exactly once" becomes a compile-time property rather than a review finding. escape_ninja_value also rejects control characters, closing the injection hole at the same seam.

Four milestones, sequenced so each is a coherent plateau:

  1. EP-M0 — real-ninja differential oracle and the red regression matrix.
  2. EP-M1 — lower $in/$out for script: recipes (must precede escaping).
  3. EP-M2 — the escaping seam; command_list_entry drops its hand-baked $$.
  4. EP-M3 — fallible path emission, converting corruption into a diagnostic.
  5. EP-M4 — users' guide migration, design-doc update, ADR-011, roadmap tick.

Verification

The oracle is the real ninja binary (ninja -t commands), not a hand-written lexer model — a model would be written from the same mental model as the escaper, so a shared misconception would pass green, and it structurally cannot express "Ninja accepts this file". Seven obligations (I1–I7) are stated with non-vacuity controls and seeded faults for each.

Kani and Verus are explicitly rejected with reasons: the introduced function is a pure total string map, and a Verus proof would require axiomatising str::replace then proving the axiom implies the specification.

Decisions needing approval before implementation

  1. D-BACKTICKsubstitute preserves backtick regions, so cat `basename $in` leaves $in unlowered; after escaping the shell receives a literal $in and silently produces nothing. Recommendation: reject with a typed diagnostic rather than silently diverge.
  2. D-METADATA — whether to escape description, depfile, deps, and pool. Recommendation: no. Descriptions are never $in/$out-lowered, so escaping them alone removes the working description = CC $out idiom and gives nothing back; depfile = $out.d is the canonical Ninja idiom that roadmap 3.14.6 will depend on. Scope to command and script text exactly as the approved design states, and record the gap as a follow-up.

Note on branch naming

The task brief asked for branch 3-14-5-regression-coverage-for-conditional-action-dependency-manifests and a (3.14.5) PR title, but the task body, the roadmap entry, and the requested plan filename are all 3.14.7. That 3.14.5 branch already exists on origin at a857fde carrying the separate 3.14.5 plan in #387, so pushing here would have collided with it. I treated the 3.14.5 naming as a stale carry-over and used a 3.14.7 branch. Happy to move it if that was wrong.

Validation

make markdownlint passes (82 files, 0 errors). Docs-only change; no Rust source touched, so the cargo gates are unaffected.

References

🤖 Generated with Claude Code

Summary by Sourcery

Make Ninja backend emission preserve shell dollar syntax safely after Netsuke placeholder lowering.

New Features:

  • Preserve ordinary shell dollar expressions through Ninja generation while keeping placeholder lowering in the backend-neutral IR.
  • Lower $in and $out placeholders in script recipes before Ninja emission and reject unsupported backtick usage with diagnostics.

Bug Fixes:

  • Prevent Ninja parsing and silently removing shell variables from generated commands and scripts.
  • Reject unsafe control characters and Ninja-special path characters instead of producing corrupt build files.

Enhancements:

  • Introduce a typed shell-text to Ninja-value escaping boundary and centralise residual dollar escaping across recipe forms.
  • Update user and developer guidance, architecture documentation, ADR-011, and roadmap status for the completed backend escaping behaviour.

CI:

  • Require the Ninja executable in CI integration coverage instead of silently skipping tests when it is unavailable.

Documentation:

  • Document ordinary shell dollar syntax, migration from historical $$ workarounds, placeholder-lowering behaviour, and the new backend escaping boundary.

Tests:

  • Add differential integration coverage using the real Ninja binary for shell variables, scripts, command lists, placeholder ordering, unsafe inputs, and path validation.
  • Expand property coverage to generate dollar-containing scalar commands and add a user-facing BDD scenario for shell-variable preservation.

Chores:

  • Correct documentation and fixtures that relied on Ninja-specific dollar escaping or introduced unsafe scalar command newlines.

@sourcery-ai

sourcery-ai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a detailed execution plan document for roadmap task 3.14.7 describing how to implement backend dollar escaping in the Ninja generator while preserving IR purity, including milestones, risks, verification strategy, and design decisions.

Sequence diagram for planned command generation with Ninja escaping

sequenceDiagram
    participant Manifest as Manifest
    participant IR as IrGraph
    participant CmdInterp as cmd_interpolate
    participant NinjaGen as ninja_gen
    participant Escape as ninja_gen_escape
    participant Ninja as ninja_binary

    Manifest->>CmdInterp: interpolate_command_with_bindings(template, bindings)
    CmdInterp-->>IR: Action.command(shell_text)

    IR->>NinjaGen: generate_into(graph, writer)
    NinjaGen->>Escape: escape_ninja_value(ShellText)
    Escape-->>NinjaGen: NinjaValue or NinjaGenError
    NinjaGen->>writer: write "command = " + NinjaValue

    NinjaGen->>Ninja: build.ninja
    Ninja-->>NinjaGen: parses & expands dollars correctly
Loading

File-Level Changes

Change Details Files
Introduce an ExecPlan markdown document for roadmap item 3.14.7 that specifies how to escape Ninja backend dollar syntax after Netsuke placeholder lowering, including sequencing of milestones, design constraints, risks, verification obligations, and decisions requiring approval.
  • Create docs/execplans/3-14-7-escape-backend-dollar-syntax-after-netsuke-placeholder-lowering.md with a full execution plan for implementing backend dollar escaping.
  • Document current dollar-handling failures in Ninja, including silent variable erasure and bad $-escape parse errors.
  • Define the ShellText→NinjaValue escaping seam concept, planned new module, and fallible constructor semantics to ensure dollars are escaped exactly once and control characters are rejected.
  • Lay out milestones EP-M0–EP-M4 covering test matrix creation, script placeholder lowering, escaping implementation, fallible path emission, and documentation/ADR updates.
  • Record risks (e.g., script regression, build-file injection via newlines, path/command disagreement), and a verification plan with specific test obligations (I1–I7) using the real ninja binary as oracle.
  • Capture open design decisions needing approval (backtick handling and whether to escape metadata fields like description/depfile) and branch-naming rationale.
docs/execplans/3-14-7-escape-backend-dollar-syntax-after-netsuke-placeholder-lowering.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Warning

Your free Security trial is over. An organization admin can activate billing to continue.


Comment @coderabbitai help to get the list of available commands.

codescene-access[bot]

This comment was marked as outdated.

leynos added 2 commits August 17, 2026 02:08
Draft the execution plan for roadmap task 3.14.7, which makes the Ninja
backend escape residual literal dollars as `$$` after Netsuke's own
placeholder lowering, so shell variables survive to the shell while the
IR stays free of Ninja-specific escaping.

Reconnaissance and an adversarial design review established several facts
that reshape the task beyond its roadmap wording:

- Two distinct failures exist today, not one. `$PATH` is silently erased
  by Ninja's lexer; `${CARGO:-cargo}` is a hard parse error. Assertions
  must distinguish them.
- `$in` and `$out` already work inside `script:` recipes by accident,
  via Ninja's own built-ins, because `register_action` lowers only
  command recipes. Escaping alone would regress every such script, so
  script lowering must land first.
- A scalar command containing a newline injects raw Ninja syntax into
  the generated file. The new escaping constructor is therefore fallible
  and rejects control characters.
- Escaping commands while leaving path emission raw would make the
  dependency edge and the command disagree for a path such as `input$1`,
  which an existing fixture already uses.

The plan proposes a `ShellText` to `NinjaValue` seam that makes "escaped
exactly once" a compile-time property, sequences the work as four
milestones, and grounds verification in differential testing against the
real Ninja binary rather than a hand-written lexer model. Kani and Verus
are explicitly rejected with reasons.

Two decisions are marked as needing approval before implementation:
handling of `$in`/`$out` inside backtick regions, and whether the escape
extends to `description` and `depfile`.

Refs: docs/roadmap.md 3.14.7; netsuke-design.md 2.6, 5.4.
The rebase onto `origin/main` was clean, but "Add target descriptions and
netsuke help targets" (#551) restructured `src/manifest/render.rs` and
shifted every documentation section the plan cites, so the plan's
file:line references no longer resolved.

Description and recipe rendering are now the shared helpers
`render_description` and `render_recipe`, each taking a `subject` for
diagnostics. That makes the EP-M1 render change a single-arm edit to
`render_recipe` rather than the two call sites the plan described.

Targets also gained descriptions, consumed by the new
`netsuke help targets` catalogue, while `src/ir/from_manifest.rs`
deliberately keeps target descriptions out of the generated Ninja file.
This strengthens decision `D-METADATA`: `description` now feeds a
non-backend consumer, so applying a Ninja-specific transform to it would
repeat the layering mistake the task exists to correct. The
recommendation to scope escaping to command and script text stands.

The core findings are untouched. `src/ir/from_manifest_support.rs:54`
still passes script recipes through unlowered, so script `$in`/`$out`
lowering must still precede escaping.

Milestones, verification obligations, and the two open decisions are
unchanged. The plan remains DRAFT pending approval.
@leynos
leynos force-pushed the 3-14-7-escape-backend-dollar-syntax-after-netsuke-placeholder-lowering branch from 39a2090 to 546d45b Compare August 17, 2026 00:15
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No quality gates enabled for this code.

@leynos leynos changed the title Plan: Escape backend dollar syntax after Netsuke placeholder lowering (3.14.7) Escape backend dollar syntax after Netsuke placeholder lowering (3.14.7) Aug 24, 2026
Lower Netsuke placeholders before converting completed shell text to a
Ninja binding, so shell variables survive generation without coupling the
IR to Ninja syntax.

Reject ambiguous paths and control characters, require real Ninja coverage
in CI, and document the migration from historical `$$` recipe spellings.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
ninja_dollar_escaping_tests.rs 1 advisory rule 9.39 Suppress

See analysis details in CodeScene

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.

Comment on lines +56 to +80
fn ninja_commands(ninja_file: &str, target: &str) -> Result<String> {
let workspace = required_ninja_workspace()?;
let path = Utf8PathBuf::from_path_buf(workspace.path().to_path_buf())
.map_err(|non_utf8| anyhow::anyhow!("non-UTF-8 temporary path: {non_utf8:?}"))?;
let directory = Dir::open_ambient_dir(&path, ambient_authority())
.with_context(|| format!("open Ninja workspace {path}"))?;
directory
.write("build.ninja", ninja_file)
.context("write generated Ninja file")?;

let output = Command::new("ninja")
.args(["-f", "build.ninja", "-t", "commands", target])
.current_dir(path.as_std_path())
.env_clear()
.env(SENTINEL, SENTINEL_VALUE)
.output()
.context("run Ninja command oracle")?;
if !output.status.success() {
bail!(
"Ninja rejected generated file: {}",
String::from_utf8_lossy(&output.stderr)
);
}
String::from_utf8(output.stdout).context("Ninja command output was not UTF-8")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: ninja_commands,ninja_output

Suppress

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant