Skip to content

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

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

leynos wants to merge 14 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.

@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.

@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.

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 and others added 10 commits September 19, 2026 01:29
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>
`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>
`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>
Rebase onto origin/main (0ba6672, previously 81d44f8) brought in roughly
twenty-nine commits of environment-policy, budget, and observability work
that the plan had scheduled itself or assumed was pending. Record what
changed and correct the plan accordingly.

Six corrections:

- Renumber the plan's ADR from 021 to 027. adr-021 was taken by the
  upstream fetch-policy ADR at the anchor commit and five more landed on
  top of it, so adr-026 is now the highest.
- R9's collision is a convergence: src/manifest/registration.rs already
  exists with exactly the four planned members, so EP-M1's extraction step
  becomes an extension step. src/manifest/mod.rs fell from the 400-line
  cap to 259 lines.
- env_var_with already takes an &EnvAccessPolicy evaluated before the
  reader. Added constraint 12 so env(name, default=...) cannot bypass
  ADR-026, and updated the EP-M1 signature and test plan to cover the
  blocked-with-default case.
- Every env() lookup already reaches env_telemetry::record_env_lookup with
  a closed outcome vocabulary, so EP-M1 must not add a fifth outcome and
  EP-M5's new counter is conditional on stating what it adds.
- Added constraint 13: a new metric series is silently dropped unless
  src/observability_recorder.rs admits it, which this plan had no rule
  for. EP-M5's counters step now carries the admission requirement.
- Re-took every document-line and source-line citation against 0ba6672,
  recording the pre-rebase anchors in the Conformance basis table so a
  reader can tell drift from error.

Also fixed three internal inconsistencies in the milestone text, all
pre-review drafts the reviewed interface section had already superseded:
EP-M3 named src/recipe_shell/quoting.rs and a src/recipe_shell/ promotion
where the reviewed boundary requires a src/shell_word.rs leaf and keeps
recipe_shell.rs a data-only single file, and EP-M3 and EP-M5 named
src/stdlib/shell/ where the reviewed boundary names src/stdlib/recipe_text/.

Status moves from DRAFT to IN PROGRESS: the requester directed
implementation to proceed. No code has been written yet.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…dule

`src/manifest/registration.rs` already owns the manifest's helper
registration concerns, but `env()` and `glob()` were still bound inline in
`evaluate_manifest`. Move them behind two named functions so EP-M1's `env`
changes land in one place and the relocation is reviewable on its own.

This is a pure move: the closures, the policy ordering, and the glob anchor
behaviour are unchanged. The only other edit is the import path in
`src/manifest/tests/env_function.rs`, which reached `env_var_with` through a
private re-export that this move removes; it now names the `env_reader`
module directly, matching `env_telemetry.rs`.

`src/manifest/mod.rs` falls from 259 to 252 lines.
@leynos leynos changed the title Plan: Make Jinja command helpers match the documented ergonomics (3.14.8) Make Jinja command helpers match the documented ergonomics (3.14.8) Sep 19, 2026
The manifest `env()` helper accepted exactly one positional argument, so a
manifest could not express "use this value, or fall back to that one". This
adds the `default` keyword argument the documentation already promised.

- `env_var_with_default` replaces `env_var_with`, taking the fallback as an
  `Option<String>` and keeping the ADR-026 access policy evaluated *before*
  the reader, so `env('BLOCKED', default='x')` cannot bypass a block.
- A substituted fallback records the lookup as `success` plus a
  `fallback_used` tracing event. It does not add a fifth outcome to the
  bounded `netsuke_manifest_env_lookups_total` series, whose label set the
  application recorder admits against.
- `env_default_from_kwargs` reads `Option<Value>` rather than
  `Option<String>`, because MiniJinja's `Option<String>` conversion silently
  stringifies numbers, booleans, sequences, and mappings into text destined
  for a shell recipe. Non-string defaults are rejected by kind.
- `manifest.env.args_error` and `manifest.env.default_not_string` are added
  to all 35 catalogues, carrying the `[netsuke::jinja::env::args]` code in
  the Fluent text so the diagnostic stays greppable in every locale.
- The stdlib query surface's disabled `env` stub is widened to accept
  `Kwargs`, so it reports "env is disabled" rather than a detail-free
  "too many arguments".
- Three `manifest.feature` scenarios cover fallback, present-value-wins, and
  non-string rejection; the stdlib harness never registers `env`, so these
  belong to the manifest suite, not `stdlib.feature`.

Gate fixes found by the first `make lint`: `doc_markdown` on `MiniJinja`,
`option_if_let_else`, `too_many_arguments`, and two structural extractions —
`substitute_fallback` (to satisfy `single_match_else` and
`option_if_let_else`, which contradict each other on one site) and
`default_as_string`. Whitaker's `no_expect_outside_tests` flagged the shared
`assert_resolution` helper, which now compares a `Resolved` enum instead.

All seven commit gates pass: check-fmt, lint (incl. Whitaker), typecheck,
markdownlint, doc-coverage (98.80%), test (3212/3212 plus doctests), nixie.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@leynos
leynos force-pushed the 3-14-8-jinja-command-helpers-to-match-documented-ergonomics branch from f574e85 to 5d66db4 Compare September 19, 2026 03:02
codescene-access[bot]

This comment was marked as outdated.

leynos and others added 3 commits September 19, 2026 06:35
CodeRabbit's `--agent` pass on 5d66db4 returned 13 unique findings: 7 were
plan-document drift, 2 were real code findings, and 4 were rejected.

Plan fixes (the ExecPlan is the deliverable for the milestones still ahead, so
drift in it is a real defect):

- Restore the `env_var_with_default` sketch's policy parameter and correct its
  doc line to "defined, non-string" `default`, matching D4's own EP-M1 note:
  MiniJinja's `Option<Value>` read maps absent, `none`, and undefined alike
  onto `None`, so the sketch's claimed undefined arm cannot fire.
- Correct the `dialect` sketch in EP-M4 to `Option<Value>`; the
  `Option<String>` read it showed would stringify a non-string rather than
  raise.
- Rename `ShellDialect::ACCEPTED` to `ALL` at the three sites that still used
  the pre-review name, which the plan declares but never lists.
- Restate the OBL-ENV-DEFAULT partition: the third default state,
  `default=none`, is not a distinct arm at the `env_var_with_default` seam,
  because the `Option<Value>` read collapses it onto "no default". It is
  enumerated once at the template layer instead, so the seam's domain is the
  8-case `{present-nonempty, present-empty, absent, not-unicode}` x
  `{no default, default='fallback'}`.
- Drop two literal cut-and-paste duplications.

Code fixes:

- `tests/manifest_env_tests.rs` was 420 lines against AGENTS.md's explicit
  400-line cap. The `default`-argument cases move to
  `tests/manifest_env_tests/default_argument.rs` behind a `#[path]`
  declaration, halving the file to 246 lines; the new file is 198.
- The fallback path had no telemetry coverage, so a test could have pinned a
  fifth outcome onto the bounded series without any test objecting.
  `a_substituted_fallback_counts_one_success_series` pins that a substituted
  default is a `success` and nothing else. Proved non-vacuous by a negative
  control that swapped the fallback path's outcome constant; the test failed
  with exactly the intended assertion, and passed again after revert.

Two *new* clippy findings then appeared, both because lifting code out of a
`#[test]` body also lifts it out of `clippy.toml`'s `allow-expect-in-tests`
exemption: `needless_pass_by_value` on `render_first_command`'s reader, and
`expect_used` on the shared `ensure_template_is_rejected`. Both were fixed
structurally rather than by adding `#[expect]` — the reader is taken by
reference, and the helper returns the error for the caller to branch on via
`let … else`.

Rejected: four findings claimed translation wording is wrong in the `nl`, `nb`,
`id`, and `it` catalogues. Each quoted the text it claimed to find, and that
text exists in no catalogue in any locale; the recorded disposition is in the
plan's `Artefacts and notes` entry 6 so the decision is auditable.

All seven commit gates pass: check-fmt, lint (all four sub-targets), typecheck,
markdownlint, doc-coverage (98.80%), test (3213/3213 plus doctests), nixie.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Add the `compact` filter to the shared collection filters, dropping only
`none`, undefined, and the empty string. `0`, `false`, whitespace-only
strings, and empty containers are values and are retained — the asymmetry a
truthiness reading of "blank" would silently invert, so the predicate is
named and documented rather than inlined.

Non-sequence subjects are rejected by `ValueKind`, per decision D8. This is
not the same check as `try_iter()`, which happily accepts a map (yielding its
keys) and a string (yielding its characters); iterating first would make
`{{ my_map | compact }}` quietly return the map's keys.

Registering through `collections::register_filters` rather than beside the
private filter bodies is what carries `compact` to the manifest-query surface
as well; a table row in `stdlib_manifest_query_tests` holds that claim to
account rather than assuming it.

The new cases pushed the flat `tests/std_filter_tests/collection_filters.rs`
past AGENTS.md's 400-line cap, so it became a directory module: the `uniq`,
`flatten`, and `group_by` tests move unchanged into `group_by_tests.rs`, and
the new witness case, the `OBL-COMPACT` property, and the non-sequence table
land in their own files.

Evidence: `std_filter_tests` 130/130, `stdlib_manifest_query_tests` 4/4, and
the naive-truthiness negative control failing three tests as designed
(`'a,0,False,b'` rendered as `'a,b'`). Full gate set green: check-fmt, lint
(all four sub-targets), typecheck, markdownlint, doc-coverage 98.80%,
test 3225/3225 plus doctests, and nixie 143 diagrams.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Close the EP-M2 Progress entry now that the milestone is committed as
683a166, and correct the entry's own claim: the nine-gate evidence belongs
to the commit, so the entry becomes `[x]` with the counts and log paths that
back it.

Two restart notes are corrected rather than carried forward. The registration
lives in `src/stdlib/register.rs` calling `collections::register_filters`, not
in a `register_helpers` in `src/stdlib/mod.rs` as the entry previously said.
And the deterministic witness case deliberately omits the whitespace-only
member that the property generates, because `join` renders `" "` exactly as it
renders `""` — an expectation including it cannot distinguish "kept the
whitespace" from "dropped it and kept nothing", so the case would have passed
for the wrong reason. The property carries that boundary instead.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@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.

Code Health Improved (1 files improve in Code Health)

Gates Passed
6 Quality Gates Passed

See analysis details in CodeScene

View Improvements
File Code Health Impact Categories Improved
group_by_tests.rs 9.39 → 10.00 Code Duplication

Absence of Expected Change Pattern

  • netsuke/src/stdlib/register.rs is usually changed with: netsuke/src/stdlib/config/mod.rs

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.

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