Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
9cf0504
Draft ExecPlan for Jinja command helpers (3.14.8)
leynos Sep 8, 2026
c66a934
Correct two source claims in the 3.14.8 ExecPlan
leynos Sep 9, 2026
13dbd31
Replace guessed quoting expectations with verified values
leynos Sep 9, 2026
fae4d33
Redraw the quoting seams after structural review
leynos Sep 9, 2026
fa3408b
Correct three falsified MiniJinja assumptions and the security claim
leynos Sep 9, 2026
1035d45
Apply the community-of-experts review to the 3.14.8 ExecPlan
leynos Sep 9, 2026
5b2c541
Use the Oxford spelling of "handwritten" in the ExecPlan
leynos Sep 9, 2026
451c434
Rename the AX-n axiom labels to AXIOM-n
leynos Sep 9, 2026
3e3c9a3
Reconcile the 3.14.8 ExecPlan against the rebased base
Sep 18, 2026
c56030a
Move the env and glob registrations into the manifest registration mo…
Sep 19, 2026
b15e76f
Implement EP-M1: env(name, default=...) with a type-checked default
Sep 19, 2026
d2c5909
Apply the EP-M1 CodeRabbit review
Sep 19, 2026
1f352eb
Implement EP-M2: values | compact
Sep 19, 2026
9e4efd2
Record the EP-M2 commit and its gate evidence
Sep 19, 2026
4ee606e
Implement EP-M3: extract the shared recipe-shell quoting seam
Sep 26, 2026
524c34c
Record the EP-M3 rebase onto origin/main
Sep 26, 2026
9fffc2a
Split the query-disabled helpers out of register.rs
Sep 26, 2026
0c0aa93
Restore the moved query stubs and make their oracle live
Sep 26, 2026
74a1506
Record the query-stub drift and the oracle gap it exposed
Sep 26, 2026
e324a44
Clear the last red gate and record why it was hidden
Sep 27, 2026
0802988
Record the green gate set at the head that cleared markdownlint
Sep 27, 2026
a4304cb
Act on the CodeRabbit review and correct a false rationale
Sep 27, 2026
02fd12c
Add shell_quote/shell_join filters and their localizations
Sep 27, 2026
8c96f5e
Thread the resolved recipe shell into manifest rendering
Sep 27, 2026
9e26418
Add the shell filter property and contract tests
Sep 27, 2026
4804253
Add the shell filter composition tests
Sep 27, 2026
db8d2be
Add the shell filter behavioural scenarios
Sep 27, 2026
33c2515
Pin the manifest-query shell surface and repair formatting
Sep 27, 2026
3c414bf
Document the query-surface quoting agreement
Sep 27, 2026
056c1b2
Record the EP-M4 surprises and close the milestone
Sep 27, 2026
b7feedc
Fix the clippy findings EP-M4 never linted
Sep 27, 2026
0161274
Clear the 31 clippy and Whitaker findings in the EP-M4 tests
Sep 27, 2026
5544f06
Record the EP-M5 reconnaissance before starting it
Sep 27, 2026
f5d8e87
Fix the Windows-only test module the quote rename missed
Sep 27, 2026
2b03604
Record the Windows-only compile break in the ExecPlan
Sep 27, 2026
7aed183
Bring the query-surface contract under the 400-line cap
Sep 27, 2026
a33e0c8
Split the composition suite's transport helpers into a child module
Sep 27, 2026
14bf1b6
Split the shell-filter property suite into obligation modules
Sep 27, 2026
6d62586
Gate the POSIX round trip to Unix so Windows can run the suite
Sep 27, 2026
82d0c8c
Record the Windows merge-gate fix and renumber the ADR to 041
Sep 27, 2026
36f69f3
Record the recipe shell quoting surface as ADR-041
Sep 27, 2026
d3ac141
Ship the recipe-shell quoting filters and their dialect counter (3.14…
Sep 27, 2026
7134b67
Clear the lint cascade and the spelling sweep (3.14.8, EP-M5)
Sep 27, 2026
eaa0cbb
Reject a non-string dialect instead of stringifying it
Sep 27, 2026
9488ec8
Keep an empty byte array out of `compact`'s blank set
Sep 27, 2026
a44da85
Correct the `posix_shell` doc and record the Gaelic keyword term
Sep 27, 2026
81a2433
Record the rebase, the gate run, and the review dispositions (3.14.8)
Sep 27, 2026
c32caab
Repair the two gate failures the repair pass introduced (3.14.8)
Sep 27, 2026
5045cb8
Canonicalise the two Markdown files mdtablefix rejected (3.14.8)
Sep 27, 2026
3379f62
Record why markdownlint and mdtablefix are not redundant (3.14.8)
Sep 27, 2026
6ba7b51
Record the 31ea3dc8 seven-gate green run (3.14.8)
Sep 27, 2026
367102c
Disposition the five CodeRabbit findings and split the query test (3.…
Sep 27, 2026
20d0728
Correct the review attribution in the disposition entry (3.14.8)
Sep 27, 2026
06f9b85
Drop the blank line `cargo fmt --check` rejected (3.14.8)
Sep 27, 2026
75c1ce8
Record the split's formatting failure and the log-provenance trap (3.…
Sep 27, 2026
cd1ec21
Strengthen the Indonesian finding's evidence (3.14.8)
Sep 27, 2026
5875903
Repair the two Markdown defects the re-run found (3.14.8)
Sep 27, 2026
f24a728
Exempt the shell-filter test harness from CodeScene's string rule (3.…
Sep 27, 2026
3bdd0d0
Fix the Korean topic marker and record the review dispositions (3.14.8)
Sep 27, 2026
8dfeb44
Record the c3078c1f gate run and tick the stale repair-pass box (3.14.8)
Sep 27, 2026
e1c2bd2
Record the clean seven-gate run at 323169b4 (3.14.8)
Sep 27, 2026
75f1ff6
Apply the four valid findings from the 75e0b671 review (3.14.8)
Sep 27, 2026
9d46d7d
Point the seam-test cross-reference at the comment that exists (3.14.8)
Sep 27, 2026
8982bec
Correct the disposition log and the divergence claim it defends (3.14.8)
Sep 27, 2026
4b19fbd
State which declined findings are duplicates of each other (3.14.8)
Sep 27, 2026
03d10f2
Drop the first person from the new disposition item (3.14.8)
Sep 27, 2026
83bbd57
Record the third triage correction as a Progress entry (3.14.8)
Sep 27, 2026
9437042
Record the c7720535 gate run and the near-miss it produced (3.14.8)
Sep 27, 2026
604bbb4
Record the fourth check the disposition log needed (3.14.8)
Sep 27, 2026
53fe353
Correct the fourth failure mode's own false claim (3.14.8)
Sep 27, 2026
ae45f7d
Record the 77089f0a gate run and the triage of a fresh review (3.14.8)
Sep 27, 2026
dcdcd9b
Record the final 5f793f01 gate run as the gate of record (3.14.8)
Sep 27, 2026
21a488c
Record the c286919b gate run and close the re-gate recursion (3.14.8)
Sep 27, 2026
fa11a7e
Record the rebase onto 6357fda5 and its four-gate validation (3.14.8)
Sep 30, 2026
aff9e51
Extract shell-quote dialect metric predicate
Sep 30, 2026
de215ae
Record the CodeScene fix in the 3.14.8 plan
Sep 30, 2026
816a87d
Correct the 3.14.8 plan's environmental caveat
Sep 30, 2026
efa4586
Fix the 3.14.8 plan's spelling of "canonicalizes"
Sep 30, 2026
ecace65
Group the stdlib counter admission rules
Sep 30, 2026
e2cb649
Record the stdlib admission grouping in the 3.14.8 plan
Sep 30, 2026
d462737
Flatten the dialect assertion to clear a CodeScene bump
Sep 30, 2026
e713a37
Record the CodeScene regression and correct a wrong figure
Sep 30, 2026
a6173e8
Record the 3.14.8 gate results and correct two wrong figures
Sep 30, 2026
74d6a6a
Record the re-gated evidence for the corrected plan
Sep 30, 2026
8584b26
Record that the PR's changes-requested review is stale
Sep 30, 2026
c81792b
Record green CI and the recovered gates for 664b9422
Sep 30, 2026
720be76
State the last verified revision instead of re-gating again
Sep 30, 2026
056e3e9
Record green CI on the tip and the linter's version drift
Oct 1, 2026
6d53180
Close the record with the tip's CI run and the exact linter drift
Oct 1, 2026
0e78e93
Rebase the branch onto the moved main and record the audit
Oct 1, 2026
8e6e86b
Cover the Iterable arm and the retained empty containers
Oct 1, 2026
648818c
Document the env default, compact filter, and helper registration
Oct 1, 2026
2829b7f
Cover undefined as a dropped member, not only as a subject
Oct 1, 2026
18e0aec
Satisfy clippy in the compact coverage tests
Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .codescene/code-health-rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
}
]
},
{
"matching_content_path": "tests/shell_filter_property_tests/property_support.rs",
"matching_content_path_doc": "Test-only harness for the shell_quote and shell_join property suites, where the strings are the subject matter rather than a missing domain language. Nine of its twelve parameters are &str, and six of those are adversarial by design: an unconstrained template, a shell word drawn from a metacharacter alphabet, the encoder's own output read back through a POSIX shell, a PowerShell literal to decode, and a script to run. Constraining any of them through a newtype would defeat the suite, which exists to feed the filters inputs that are not well-formed words; a transparent wrapper would add conversion noise at every call site while enforcing nothing, exactly as in src/ir/cmd_interpolate_property_support.rs. The three remaining strings are dialect selectors, which RecipeShell could type. Measured rather than assumed, that is not the remedy: typing both selectors moves the ratio from 75.0% to 58.3%, still far above the 39.0% threshold, because the floor is set by the irreducible adversarial strings and not by the selectors. Reassess if this harness gains production callers, or if the filters stop accepting unconstrained text.",
"rules": [
{
"name": "String Heavy Function Arguments",
"weight": 0.0
}
]
},
{
"matching_content_path": "src/ir/cmd_interpolate_property_support.rs",
"matching_content_path_doc": "Private test-only generated strategies and an independent POSIX scanner oracle must accept unconstrained templates, raw bindings, replacement values, and fragments. Those adversarial values intentionally include placeholders, backticks, quotes, empty text, and malformed forms. Newtypes would add conversion and borrowing noise without enforcing a constraint; reassess if this code becomes production-facing or its inputs gain a meaningful invariant.",
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ _If you are upgrading: see the
baseline PEP 649 defers evaluation, so only resolving the annotation catches
it
([#730](https://github.com/leynos/netsuke/issues/730), [ADR-038](docs/adr-038-runtime-annotation-introspection-in-workflow-contracts.md))
- Add the `shell_quote` and `shell_join` recipe-text filters, and the `compact`
collection filter. `shell_quote` encodes one string as one shell word and
`shell_join` encodes a sequence as a command line, each for a `dialect` of
`sh` or `powershell` that defaults to the dialect implied by the active
recipe shell. The documented `shell_escape` name is superseded rather than
implemented, because it would quote for the wrong interpreter on Windows
([#593](https://github.com/leynos/netsuke/issues/593), [ADR-041](docs/adr-041-canonical-recipe-shell-quoting-surface.md))
- Add the `default=` keyword argument to the `env()` template function, leaving
the existing missing-variable and invalid-UTF-8 diagnostics unchanged
([#593](https://github.com/leynos/netsuke/issues/593))
- Count `shell_quote` and `shell_join` dialect resolutions in the bounded
`netsuke_manifest_shell_quote_dialect_total` counter, labelled by `dialect`
and by whether the call site named the dialect or accepted the default, so
the manifests whose generated text is not pinned to an encoding are measurable
([ADR-041](docs/adr-041-canonical-recipe-shell-quoting-surface.md))

### Added

Expand Down
1 change: 1 addition & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ disallowed-methods = [
{ path = "std::env::set_var", reason = "use a stub environment in tests" },
{ path = "std::env::remove_var", reason = "use a stub environment in tests" },
{ path = "std::env::set_current_dir", reason = "inject a base-directory seam; confine CWD changes to Command::current_dir" },
{ path = "shell_quote::QuoteRefExt::quoted", reason = "recipe-shell word quoting lives in shell_word::quote_word" },
]
277 changes: 277 additions & 0 deletions docs/adr-041-canonical-recipe-shell-quoting-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
# Architectural decision record (ADR) 041: Canonical recipe shell quoting surface

## Status

Accepted.

## Date

2026-09-27

## Context and problem statement

A manifest renders its string fields as MiniJinja templates, and a rendered
value frequently becomes part of a shell recipe. Before this decision the
template surface had no way to quote such a value, so an author who needed a
literal space, quote, or `$` in an argument had to hand-roll the escaping in
the manifest. Hand-rolled escaping is where command injection lives, and the
manifest author is the party least able to verify it.

The design document named the gap and the intended remedy. `DD-4.5` called a
quoting filter "a non-negotiable security feature to prevent command injection
vulnerabilities". `RFC-0006-8.9` proposed the name `shell_quote` with a
`dialect` argument, and `RM-6.8.3` repeated both. Three sources therefore
agreed on the shape of the surface before any of it existed; none of them
agreed on its scope, and the RFC's premise about which encoders are compiled in
was false.

Three questions had to be answered, and each is a decision below: what the
filter is called, which dialects it accepts and what it defaults to, and whether
`bash` is one of those dialect names.

### The RFC's stated premise is false in the code

`RFC-0006-8.9` says "`dialect` currently accepts only `sh`, matching the single
`shell-quote` feature Netsuke enables." The feature half is right —
`Cargo.toml` enables only `sh` — but the conclusion does not follow. Two facts
in the code contradict it.

`src/recipe_shell.rs` establishes that the default Windows recipe interpreter
is Windows PowerShell, and `RecipeShell::host_default()` returns
`RecipeShell::PowerShell` there. A manifest on Windows therefore executes under
an interpreter with entirely different quoting rules.

`src/ir/cmd_interpolate/` already implements a second, non-`shell-quote`
encoding for exactly that case. The capability the RFC described as absent was
already compiled in and already in use by the IR lowering path.

So an `sh`-only filter would emit POSIX quoting into a recipe that Windows
PowerShell then parses. Nothing would fail loudly; the quoting would simply be
read as data, and the argument the author believed was protected would arrive
corrupted or expanded. That is a silent injection-safety defect in the helper
whose stated purpose is to prevent injection.

### Nothing else in the ecosystem offers a dialect argument

Every comparable tool ships a one-argument function with no dialect selector:
`shlex.quote` and `shlex.join` in the standard library, Just's `quote()`,
bazel-skylib's `shell.quote`, Nix's `escapeShellArg`, and Ansible's `quote`.
The dialect argument is this surface's one point of divergence from the field,
and it is the reason the next decision is not free. It was considered for
removal and kept; the alternative is recorded below.

## Decision drivers

- Make the safe thing the easy thing. An author with a hard-to-quote argument
should have a one-call answer, because the alternative is hand-rolled
escaping.
- Never emit one dialect's quoting into another dialect's interpreter. A
quoting helper that is wrong silently is worse than no helper, since it
invites reliance.
- Keep exactly one implementation of the encoding. IR lowering and the
template filters must not drift apart, because a recipe assembled from both
would then carry two incompatible quotings.
- Do not put a name on the public template surface that a known-correct source
says is already superseded.
- Defer no breaking rename to a later release. The template surface becomes
documented and executed in the same milestone that ships it.

## Requirements

### Functional requirements

- A manifest can quote one string into one shell word, and quote a sequence
into a command line of shell words, without hand-rolled escaping.
- The dialect used is selectable at the call site, and the spelling is stable
enough to document.
- A call that cannot succeed — a non-string subject, an unknown dialect, a
positional argument — fails with a diagnostic naming the filter and the
received kind, rather than coercing the value.

### Technical requirements

- Exactly one implementation of recipe-shell word quoting is compiled in;
every other call site delegates to it.
- The names shipped match what `RFC-0006-8.9`, `RFC-0006-13.3`, and `RM-6.8.3`
say, or the deviation is recorded where a reader will find it.
- The decisions are discoverable from the documentation index and from the
code they govern.

## Options considered

### Option A: `shell_escape`, single dialect (the roadmap's original name)

Ship the filter under the name the roadmap first used, with POSIX quoting only.

This is rejected on two independent grounds, either sufficient. The name is
known-superseded: `RFC-0006-13.3` states that the RFC "contributes the
canonical name `shell_quote` and the `dialect` argument; the roadmap task
should adopt them so the two do not diverge", and `RM-6.8.3` repeats it.
Shipping `shell_escape` would put a superseded name on the public surface and
force a breaking rename in a later release. `RM-3.14.8` permits "implement **or
remove**" the name; superseding it is a removal.

The single dialect is the more serious problem, for the reason above: on
Windows the filter would quote for the wrong interpreter and fail silently.

### Option B: `shell_quote`, `sh` only, error on a PowerShell host

Ship the canonical name but refuse to render when the active recipe shell is
PowerShell, so the filter cannot be used incorrectly.

This avoids the silent corruption but makes the filter unusable in the default
configuration on Windows, which is the platform where manifest authors most
need quoting, since it is the platform whose paths contain spaces and
backslashes. Refusing at render time is safe and useless.

### Option C: `shell_quote`, two dialects, host-dependent default (chosen)

Ship the canonical name with both dialects compiled in, defaulting to the
dialect implied by the active `RecipeShell`.

This is the only option that is both correct on every host and usable on every
host. Its cost is the dialect argument, which no comparable tool has, and the
instability of the omitted-dialect default, which is recorded as a decision in
its own right rather than left implicit.

### Option D: `shell_quote` with no dialect argument

Ship the canonical name and always follow the active recipe shell, dropping the
argument entirely.

This is the smallest surface and the closest to the ecosystem's convention. It
was considered seriously and not adopted: the requester chose the two-dialect
surface explicitly, and both `RFC-0006-8.9` and `RM-6.8.3` specify the
`dialect` argument by name, so dropping it would be a second deviation on top
of Option C's. It would also remove four of the six diagnostic keys and the
dialect-totality obligation, so it is a real reduction rather than a cosmetic
one.

The safety argument for Option D is adopted instead: every documented example
and every snapshot pins `dialect` explicitly, and the instability of the
omitted-dialect default is stated in the consequences below.

| Topic | A: `shell_escape`, `sh` | B: `sh` only, error | C: two dialects | D: no argument |
| ----------------------------- | ----------------------- | ------------------- | ---------------- | ---------------- |
| Name matches RFC and roadmap | No | Yes | Yes | Yes |
| Correct on a PowerShell host | No, silently | N/A, refuses | Yes | Yes |
| Usable on a PowerShell host | No | No | Yes | Yes |
| Breaking rename deferred | Yes | No | No | No |
| Call site states its encoding | No | No | Yes | No |
| Diagnostic keys introduced | 6 | 6 | 6 | 2 |
| Diverges from ecosystem norm | No | No | Yes | No |
| Reader can tell it was chosen | No, reads as legacy | Yes, this record | Yes, this record | Yes, this record |

*Table 1: Comparison of the options considered.*

## Decision outcome

Adopt **Option C**.

The helper ships as `shell_quote` and `shell_join`. Both accept a `dialect`
keyword argument taking `sh` or `powershell`, and both default to the dialect
implied by the active `RecipeShell` when the argument is omitted. The `sh`
dialect is documented as an *encoding* — the output is a lowest common
denominator for Bash, Z Shell, and `/bin/sh`-like shells — rather than as a
named interpreter.

`dialect='bash'` is **not** accepted. `RecipeShell::Bash` maps to the `sh`
dialect, because `sh` output is valid Bash. The name is refused because the
`shell-quote` crate's `Bash` encoder emits a different, `$'...'`-style form
that Netsuke does not compile in; accepting the name now would lock in a
meaning that a real `bash` dialect would later have to break.

Every `shell_escape` reference in the design and user guides is replaced by
`shell_quote`, and the filter is implemented once, in `src/shell_word.rs`, with
the template filters and the IR lowering path both delegating to it.

## Rationale

The decision turns on what a quoting helper owes its caller. Its value is
entirely in being right; a helper that is right on the author's machine and
wrong on a colleague's is worse than nothing, because it teaches reliance it
cannot support. Options A and B each break that promise in a different
direction — A silently, B loudly — and only Option C keeps it on every host.

The name follows from a different source with the same force. Three documents
agree on `shell_quote`, one of them an RFC that states the roadmap "should
adopt" it so the two do not diverge. Diverging would mean a public name, a
documented example, and a test suite that all have to change later, for no
benefit that any of the rejected options could name.

The dialect argument is the part that is genuinely a cost, and it is accepted
rather than waved away. It diverges from every comparable tool, it doubles the
diagnostic keys, and it introduces the one axis of this surface with no
versioning story: a manifest that omits `dialect` gets whatever the active
`RecipeShell` implies, which may change between releases. Option D would remove
that axis, and the argument for it is real. It was not adopted because two
normative documents specify the argument and the requester chose it explicitly,
which makes removing it a larger unilateral change than the cost justifies. The
mitigation is disclosure: the instability is stated here, every example pins
the dialect, and the diagnostic enumerates the accepted names so a reader
cannot guess wrong silently.

Option A is rejected on the name and on the Windows defect, and the two are
independent — either alone would sink it. Option B is rejected on usability
rather than on correctness; it is the safe version of the wrong answer. Option
D is rejected on authority rather than on merit, and the record says so, so
that a future reader who prefers it knows it was weighed rather than missed.

## Consequences

- `shell_quote` and `shell_join` are available to every manifest template, and
the encoding is selected by `dialect`, defaulting to the active recipe shell.
- **The omitted-dialect default is not stable across releases or hosts.** A
manifest whose generated text must be byte-stable must pin `dialect=`. This
is the accepted cost of Option C, and the one behaviour here that a future
release may change without a manifest edit.
- `RecipeShell::Bash` maps to the `sh` dialect today. If a real `bash` dialect
is added, that mapping changes, which is a concrete instance of the point
above.
- The name `shell_escape` no longer appears in the design or user guides, which
name `shell_quote` only. It is retained here, in `RM-6.8.3`, and in
`RFC-0006-8.9` / `RFC-0006-13.3`, because those are the records of the
supersession itself; a reader who arrives holding the old name finds the
decision rather than a gap.
- `src/shell_word.rs` is the single implementation of recipe-shell word
quoting, and a constraint test holds the delegation to one call site per
layer rather than to a convention.
- The `shell-quote` crate's `Sh` encoder is *suffix*-quoting rather than
canonically enclosing: it leaves the longest safe prefix bare and quotes only
the remainder, so `a b` becomes `a' b'` and not `'a b'`. The contract is
therefore round-tripping, not any particular output shape.

## Known risks and limitations

- The surface is a *safe primitive*, not an enforced control. It quotes what it
is given; it does not detect or prevent an author interpolating a filter
inside shell double quotes, where the quoter's own quote characters are data
and the argument is corrupted. That failure mode is pinned as a documented
defect with a test rather than repaired, because repairing it would mean
tracking shell context, which is a different and much larger mechanism.
- The only oracle for the PowerShell encoder on most hosts is an independently
written decoder, not an interpreter. Where a real interpreter is present the
two are compared, but the property is discharged by the model on hosts
without one, and the residual gap is recorded rather than closed.
- The dialect set is a claim about which encoders are compiled in. It is
correct for the current `Cargo.toml` feature selection and would need
revisiting if a further encoder were enabled.
- Accepting the dialect name case-insensitively is a convenience that the
design documents do not require; a reader who expects exact matching may be
surprised.

## References

- [ADR-014](adr-014-backend-text-escaping-seam.md) defines the single-line
recipe admissibility rule that the encoding is layered on top of.
- [ADR-019](adr-019-structured-command-shell-selection.md) owns the shell
registry, which accepts `bash` where this decision rejects it. The two
surfaces are deliberately not the same, and the divergence is recorded here.
- [`RFC 0006`](rfcs/0006-ansible-inspired-template-standard-library.md) §8.9
proposed the `dialect` argument and the `shell_quote` name; §13.3 states that
the roadmap task should adopt both.
- [`src/shell_word.rs`](../src/shell_word.rs) holds the single encoding.
[`src/stdlib/recipe_text/`](../src/stdlib/recipe_text/) is the
template-facing adapter that validates arguments and delegates to it.
- [`src/recipe_shell.rs`](../src/recipe_shell.rs) is the data-only interpreter
vocabulary, including `host_default()`.
3 changes: 3 additions & 0 deletions docs/contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ operator, user, and contributor references are easier to find.
Runtime annotation introspection declared unsupported for the workflow
contract tests, with the loader gate's stale `TYPE_CHECKING` claim corrected
and a revisit gate that reopens on a real consumer.
- [ADR-041](adr-041-canonical-recipe-shell-quoting-surface.md): `shell_quote`
and `shell_join` as the canonical recipe quoting surface, with two dialects
and a host-dependent default.

## Proposals

Expand Down
Loading
Loading