Skip to content

Plan: Make Jinja command helpers match the documented ergonomics (3.14.8) - #702

Draft
leynos wants to merge 8 commits into
mainfrom
3-14-8-jinja-command-helpers-to-match-documented-ergonomics
Draft

Plan: Make Jinja command helpers match the documented ergonomics (3.14.8)#702
leynos wants to merge 8 commits into
mainfrom
3-14-8-jinja-command-helpers-to-match-documented-ergonomics

Conversation

@leynos

@leynos leynos commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Summary

This branch carries the pre-implementation execution plan for roadmap task
3.14.8, which reconciles the Jinja template helpers Netsuke documents with the
ones it actually registers. No production code changes: the branch adds one
document and authorizes the implementation described in it, subject to approval.

Roadmap task: (3.14.8)

Execplan:
docs/execplans/3-14-8-jinja-command-helpers-to-match-documented-ergonomics.md

The task closes four documentation debts: env(name) gains the default=
keyword argument its design contract already specifies; the shell_escape
filter the user guide describes as "not implemented in beta3" is resolved; and
shell_join and compact are implemented so that a manifest author can build
an optional RUSTFLAGS value without reaching for ${RUSTFLAGS:+$RUSTFLAGS },
which is a construct the Ninja backend has to escape around and which does not
work at all on Windows.

Two deviations that need a decision

Both are recorded in the plan's
Decision log
and both were confirmed by the requester before drafting.

D1 — the helper ships as shell_quote, not shell_escape. RFC 0006 §13.3
states that roadmap 3.14.8 "owns the shell-quoting capability and ships first"
and contributes "the canonical name shell_quote and the dialect argument;
the roadmap task should adopt them so the two do not diverge". Roadmap 6.8.3
repeats this. Shipping the superseded name would force a public rename later.

D2 — dialect accepts sh and powershell, defaulting to the active
recipe shell.
RFC 0006 §8.9 specifies sh as the only dialect, on the stated
premise that it matches "the single shell-quote feature Netsuke enables". That
premise does not hold: Netsuke runs Windows recipes under Windows PowerShell,
not cmd.exe and not a POSIX shell, and
src/ir/cmd_interpolate/mod.rs::quote_path already implements a second dialect
for exactly that case. An sh-only filter would emit POSIX quoting into a
PowerShell recipe, which is a silent injection-safety defect in the helper whose
stated purpose is to prevent one. RFC 0006 §§8.9 and 13.3 are amended as part of
the work.

Review walkthrough

  • Start with
    Purpose / big picture
    for the manifest a user can write afterwards and the four things they can
    observe. Note the two load-bearing details of that example: the interpolation
    sits in unquoted position, and the recipe uses ; rather than &&, because
    Windows PowerShell 5.1 has no &&.
  • Then
    The quoting that already exists,
    which inventories five encoders in the tree today and identifies the single
    one this work extracts. Constraint 4 becomes a clippy.toml gate rather than
    prose.
  • The
    Threat model
    names the attacker the design document leaves implicit. shell_quote is a
    safe primitive, not an enforced control: it protects a trusted manifest that
    interpolates an attacker-influenced value from env(), glob(), fetch(),
    or shell(). It is opt-in and nothing warns when it is omitted, so the
    unqualified phrase "non-negotiable security feature" is amended.
  • The
    Verification plan
    carries thirteen obligations, each with a negative control. The strongest is a
    property test round-tripping generated values through a real /bin/sh; Verus
    and Kani are excluded with stated reasons rather than omitted.
  • EP-M4
    explains why filter registration and runner plumbing are one milestone rather
    than two. Splitting them would ship a state where, on a Windows host with
    NETSUKE_WINDOWS_SHELL=bash, the filters quote for PowerShell while the
    recipe runs under Bash — and PowerShell's doubled single quote is valid
    POSIX syntax, so a'b silently becomes ab with no error anywhere.
  • Finish with the
    Risks,
    particularly R9: the in-flight issue-651 branch restructures the same
    manifest files, so the plan names its new module to converge with that work
    rather than conflict with it.

How the plan was produced

Reconnaissance ran as a team of read-only agents over the manifest loader, the
three stdlib registration surfaces, the recipe-shell contract, the 35-catalogue
localization gate, and the executed-documentation machinery. The draft was then
stress-tested by a six-lens community-of-experts panel — structural integrity,
alternative futures, contracts, failure modes, cost, and long-term viability.

That review falsified four claims in the first draft, each verified before being
acted on:

  • Value::try_iter() is not a sequence check. It accepts none and undefined
    as empty, a string as its characters, and a map as its keys, so
    {{ 'abc' | shell_join }} would have quoted three characters into a command
    line. The helpers now gate on ValueKind.
  • Kwargs::get::<Option<String>> does not raise on a type mismatch; it
    stringifies, so env('A', default=['a','b']) would have pasted the JSON
    fragment ["a", "b"] into a recipe — the silent coercion RFC 0006 §6.6
    forbids.
  • The plan's own acceptance transcript placed the interpolation inside double
    quotes, where the quoting inserts literal quote characters and corrupts the
    value. None of the original obligations would have caught it, because each
    exercised the filter in isolation.
  • quote_path is in src/ir/cmd_interpolate/mod.rs, not substitution.rs, so
    an implementer following the plan literally would have edited the wrong file.

The panel's preferred simplification — dropping dialect entirely, which would
cut eleven message keys to one — is preserved as decision D11 with its full
trade-off analysis. It is cheap to adopt before EP-M4 and expensive afterwards,
so it is worth settling at approval time.

Validation

Documentation-only branch, so only the Markdown gates apply. make test,
make lint, and make typecheck were deliberately not run: there is no Rust
change to gate.

  • make check-fmt: pass — cargo fmt --check and ruff format --check clean
  • make nixie: pass — all Mermaid diagrams validated
  • make markdownlint: pass — markdownlint-cli2 linted 133 files with 0
    errors, and its spelling prerequisite (the typos gate plus the 34-case
    typos_rollout suite) ran to completion. It failed twice first, both times
    in that prerequisite and both times on this branch's own file:
    both times on this branch's own new file. typos enforces en-GB-oxendict
    spelling and rejected hand-written (wanting handwritten), then read the
    bare acronym AX in the AX-1..AX-5 axiom labels as a misspelling of
    AXE. Fixed in c53b7af9 and f574e857 by correcting the spelling and
    spelling the labels out as AXIOM-n, rather than by widening the
    accepted-word list.

The expected quoting strings throughout the plan are derived from
shell-quote-0.7.2's escape_chars and Char::from, then confirmed against a
real /bin/sh, rather than guessed. Two results are counter-intuitive and are
called out so a reviewer does not "correct" them: quoting opens and closes
around runs, so a b becomes a' b'; and the equals sign is not inert, so
target-cpu=native becomes target-cpu'=native'.

Notes

The plan is DRAFT and must be approved before implementation begins. On
completion the implementor marks roadmap 3.14.8 done and adds a note to 6.8.3
recording that this task delivered the canonical name and the dialect argument.

References

Summary by Sourcery

Add a draft execution plan for implementing the documented Jinja command helpers and aligning their cross-platform shell behavior, registration, testing, and documentation.

Enhancements:

  • Add a draft execution plan for reconciling documented Jinja command helpers with the supported template ergonomics across environment defaults, collection handling, and shell recipe text.
  • Define the cross-platform shell quoting contract, including the canonical shell_quote name, sh and PowerShell dialects, recipe-shell integration, security boundaries, and localization requirements.
  • Specify milestone-based implementation, testing, documentation, and acceptance work for the planned helpers without changing production code.

Documentation:

  • Document the planned helper behavior, design decisions, threat model, verification obligations, implementation milestones, and roadmap/RFC reconciliation.

Tests:

  • Define comprehensive unit, property, integration, BDD, query-surface, and executed-documentation verification for the future implementation.

leynos and others added 6 commits September 8, 2026 17:40
Add the execution plan for roadmap task 3.14.8, which reconciles the
Jinja template helpers Netsuke documents with the ones it registers:
`env(name, default=...)`, `shell_quote`, `shell_join`, and `compact`.

Reconnaissance established three facts that shape the design:

- Netsuke runs Windows recipes under Windows PowerShell, not `cmd.exe`,
  and `src/ir/cmd_interpolate/substitution.rs` already implements a
  second quoting dialect for it. RFC 0006 section 8.9's premise that
  `sh` is the only dialect Netsuke can quote for is therefore false, so
  the plan records a deviation adding a `powershell` dialect and a
  host-default rule.
- RFC 0006 section 13.3 and roadmap 6.8.3 both direct 3.14.8 to adopt
  the canonical name `shell_quote` in place of the documented but
  unimplemented `shell_escape`.
- A new user-facing message must be added to all 35 Fluent catalogues
  or `build.rs` fails the build, which dominates the mechanical effort.

The plan is a draft awaiting approval; no implementation has begun.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`RecipeShell` is already publicly nameable: `src/lib.rs:27` declares
`pub mod recipe_shell;` and the enum is `pub`, so
`StdlibConfig::with_recipe_shell` needs no visibility widening. Drop the
escalation clause that assumed otherwise.

Also correct the `docs/users-guide.md` line reference for the `env()`
default-argument sentence from 773-775 to 774-775.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Derive the expected `shell_quote` and `shell_join` output from
`shell-quote-0.7.2`'s `escape_chars` and `Char::from`, then confirm each
by round-tripping it through a real `/bin/sh`. Two results are
counter-intuitive and were guessed wrongly in the first draft: quoting
opens and closes around runs, so `a b` becomes `a' b'` rather than
`'a b'`, and the equals sign is not inert, so `target-cpu=native`
becomes `target-cpu'=native'`.

Add OBL-NO-ESCAPE, covering the case where MiniJinja auto-escaping would
silently corrupt quoted recipe text; the manifest path is believed never
to auto-escape, but nothing in the plan guaranteed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correct the `quote_path` citation: it is in `src/ir/cmd_interpolate/mod.rs`,
not `substitution.rs`, so an implementer following the plan literally
would have edited the wrong file.

Restate the encoder inventory as five, not three. `PathSubstitutions::new`
and `quote_double_quoted_path` are two further POSIX encoders; naming
them makes clear that this plan extracts exactly one.

Redraw three boundaries the review found misplaced:

- Put the encoder in a new `src/shell_word.rs` leaf rather than in
  `src/recipe_shell.rs`, which is deliberately data-only and would
  otherwise acquire a third-party encoder dependency.
- Name the stdlib module `recipe_text`, not `shell`: `src/stdlib/command/`
  already registers a filter literally named `shell` and already holds a
  `quote.rs`, so a sibling `shell/` module inverts the naming at both ends.
- Take `ShellDialect` in `quote_word` and drop the lossy inverse;
  `RecipeShell` to `ShellDialect` is three-to-two, so the inverse could
  not have a truthful doc comment.

Reuse `validate_ninja_value` instead of reimplementing its control-character
rule, which ADR-014 already owns and which has already drifted, and make
constraint 4 a `clippy.toml` gate rather than prose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A contract review built a scratch crate against minijinja 2.24.0 and
disproved three behaviours the plan asserted. Each was load-bearing.

`Kwargs::get::<Option<String>>` does not raise a type error; it silently
stringifies every value kind, so `env('A', default=['a','b'])` would paste
the JSON fragment `["a", "b"]` into a shell recipe. That is the silent
coercion RFC 0006 section 6.6 forbids. D4 now reads `Option<Value>` and
type-checks explicitly.

`Value::try_iter()` is not a sequence check: it accepts `none` and
undefined as empty, strings as their characters, and maps as their keys.
`{{ 'abc' | shell_join }}` would have quoted three characters into a
command line. D8 gates on `ValueKind` instead.

The five messages carried no machine-readable code, but
`[netsuke::jinja::which::args]` is carried in the Fluent text and kept
verbatim by translators, and the behavioural suite switches locale. The
plan's assertions on English prose would fail in 32 catalogues. D9 adds
the `::args` and `::unquotable` wrappers.

Also: store `ShellDialect` rather than `RecipeShell` in `StdlibConfig`,
extract `src/manifest/registration.rs` before editing `mod.rs` (which is
exactly at the 400-line cap), name the query-surface dialect divergence,
log default substitution, and add a threat model that names the attacker
instead of repeating "non-negotiable security feature" unqualified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fuse the former EP-M4 and EP-M5 into one milestone. Splitting them would
have shipped a state where, on a Windows host with
NETSUKE_WINDOWS_SHELL=bash, the filters quote for PowerShell while the
recipe runs under Bash. PowerShell doubles an embedded single quote, and
`'a''b'` is valid POSIX syntax for `ab`, so shlex accepts it and the
build succeeds with a silently corrupted value. Constraint 10 now
forbids the divergence outright.

Correct the acceptance transcript, which placed the interpolation inside
double quotes where the quoting corrupts rather than protects. None of
the nine original obligations would have caught it, because every one
exercised the filter in isolation; OBL-CONTEXT now covers it.

Add OBL-JOIN-QUOTE-AGREE, OBL-KIND-GATE and OBL-COMPOSITION, assert the
behavioural scenarios on the stable `[netsuke::jinja::...]` codes rather
than English prose, add a threat model that names the attacker, and add
risks R9 to R12 covering the in-flight issue-651 collision, the
env-default footgun, the dialect mismatch, and the unguarded PowerShell
path.

Prepend an extraction step to EP-M1: `src/manifest/mod.rs` is exactly at
the 400-line cap, and naming the new module `registration.rs` converges
with the in-flight budget branch rather than conflicting with it.

Record decision D11: dropping the `dialect` argument entirely would cut
eleven message keys to one, but the requester chose the two-dialect
surface and RFC 0006 names the argument. The alternative is preserved
with its trade-offs so it can be adopted at approval time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 9, 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

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

`make markdownlint` runs `typos` over Markdown prose to enforce
en-GB-oxendict spelling, and it rejects the hyphenated form. Three
occurrences in the new plan blocked the gate before `mdlint` itself ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This documentation-only PR adds a DRAFT ExecPlan for implementing the documented Jinja command helpers, with explicit API decisions, cross-platform quoting safety, registration and runner-plumbing design, localization/documentation work, and detailed staged verification. Production behavior is unchanged and implementation is gated on plan approval.

Sequence diagram for dialect-aware recipe rendering

sequenceDiagram
    participant Manifest
    participant Loader as ManifestLoader
    participant Jinja as JinjaEnvironment
    participant Config as StdlibConfig
    participant Runner
    participant IR
    participant Ninja as NinjaGenerator
    participant Shell as RecipeInterpreter

    Manifest->>Loader: from_str_with_env_and_config
    Loader->>Jinja: render template helpers
    Jinja->>Config: dialect()
    Config-->>Jinja: ShellDialect
    Jinja-->>Loader: rendered recipe text
    Loader->>Runner: resolve recipe shell
    Runner->>IR: interpolate_command_with_bindings
    IR->>Ninja: generate recipe text
    Ninja->>Shell: execute dialect-compatible command
Loading

Flow diagram for optional RUSTFLAGS construction

flowchart TD
    Start[base_flags and env RUSTFLAGS default empty] --> Compact[compact]
    Compact --> Join[join with one space]
    Join --> Quote[shell_quote with explicit dialect]
    Quote --> Recipe[Unquoted recipe interpolation]
    Recipe --> Build[Generated Ninja recipe]
    Build --> Cargo[cargo receives one intact RUSTFLAGS word]
Loading

File-Level Changes

Change Details Files
Adds a draft execution plan for reconciling documented Jinja helper behavior with the eventual implementation.
  • Defines env(default=...), compact, shell_quote, and shell_join contracts and registration surfaces.
  • Specifies shell-dialect handling, including the shell_quote name and sh/powershell deviation decisions.
  • Lays out shared quoting extraction, runner-shell plumbing, localization updates, documentation changes, and ADR/roadmap reconciliation.
  • Provides threat modeling, milestone sequencing, integration/property-test obligations, negative controls, and acceptance gates.
docs/execplans/3-14-8-jinja-command-helpers-to-match-documented-ergonomics.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

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

`make markdownlint` runs `typos` over Markdown prose, and it reads the
bare acronym `AX` as a misspelling of `AXE`, so eleven occurrences of the
`AX-1` to `AX-5` labels blocked the gate before `mdlint` itself ran.

Spell the labels out rather than widening the accepted-word list: the
repository prefers backticks or a rewrite over a dictionary entry, and
`AXIOM-1` reads better than `AX-1` in any case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant