Plan: Make Jinja command helpers match the documented ergonomics (3.14.8) - #702
Draft
leynos wants to merge 8 commits into
Draft
Plan: Make Jinja command helpers match the documented ergonomics (3.14.8)#702leynos wants to merge 8 commits into
leynos wants to merge 8 commits into
Conversation
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>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
`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>
Contributor
Reviewer's GuideThis 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 renderingsequenceDiagram
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
Flow diagram for optional RUSTFLAGS constructionflowchart 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]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 thedefault=keyword argument its design contract already specifies; the
shell_escapefilter the user guide describes as "not implemented in beta3" is resolved; and
shell_joinandcompactare implemented so that a manifest author can buildan optional
RUSTFLAGSvalue 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, notshell_escape. RFC 0006 §13.3states that roadmap 3.14.8 "owns the shell-quoting capability and ships first"
and contributes "the canonical name
shell_quoteand thedialectargument;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 —
dialectacceptsshandpowershell, defaulting to the activerecipe shell. RFC 0006 §8.9 specifies
shas the only dialect, on the statedpremise that it matches "the single
shell-quotefeature Netsuke enables". Thatpremise does not hold: Netsuke runs Windows recipes under Windows PowerShell,
not
cmd.exeand not a POSIX shell, andsrc/ir/cmd_interpolate/mod.rs::quote_pathalready implements a second dialectfor exactly that case. An
sh-only filter would emit POSIX quoting into aPowerShell 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
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&&, becauseWindows PowerShell 5.1 has no
&&.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.tomlgate rather thanprose.
Threat model
names the attacker the design document leaves implicit.
shell_quoteis asafe 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 theunqualified phrase "non-negotiable security feature" is amended.
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; Verusand Kani are excluded with stated reasons rather than omitted.
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 therecipe runs under Bash — and PowerShell's doubled single quote is valid
POSIX syntax, so
a'bsilently becomesabwith no error anywhere.Risks,
particularly R9: the in-flight
issue-651branch restructures the samemanifest 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 acceptsnoneand undefinedas empty, a string as its characters, and a map as its keys, so
{{ 'abc' | shell_join }}would have quoted three characters into a commandline. The helpers now gate on
ValueKind.Kwargs::get::<Option<String>>does not raise on a type mismatch; itstringifies, so
env('A', default=['a','b'])would have pasted the JSONfragment
["a", "b"]into a recipe — the silent coercion RFC 0006 §6.6forbids.
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_pathis insrc/ir/cmd_interpolate/mod.rs, notsubstitution.rs, soan implementer following the plan literally would have edited the wrong file.
The panel's preferred simplification — dropping
dialectentirely, which wouldcut 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, andmake typecheckwere deliberately not run: there is no Rustchange to gate.
make check-fmt: pass —cargo fmt --checkandruff format --checkcleanmake nixie: pass — all Mermaid diagrams validatedmake markdownlint: pass —markdownlint-cli2linted 133 files with 0errors, and its
spellingprerequisite (thetyposgate plus the 34-casetypos_rolloutsuite) ran to completion. It failed twice first, both timesin that prerequisite and both times on this branch's own file:
both times on this branch's own new file.
typosenforces en-GB-oxendictspelling and rejected
hand-written(wantinghandwritten), then read thebare acronym
AXin theAX-1..AX-5axiom labels as a misspelling ofAXE. Fixed inc53b7af9andf574e857by correcting the spelling andspelling the labels out as
AXIOM-n, rather than by widening theaccepted-word list.
The expected quoting strings throughout the plan are derived from
shell-quote-0.7.2'sescape_charsandChar::from, then confirmed against areal
/bin/sh, rather than guessed. Two results are counter-intuitive and arecalled out so a reviewer does not "correct" them: quoting opens and closes
around runs, so
a bbecomesa' b'; and the equals sign is not inert, sotarget-cpu=nativebecomestarget-cpu'=native'.Notes
The plan is
DRAFTand must be approved before implementation begins. Oncompletion 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:
shell_quotename,shand PowerShell dialects, recipe-shell integration, security boundaries, and localization requirements.Documentation:
Tests: