Forthcoming capability
Preview targeted at v0.1.x Proposed RFC

Standard Library Expansion

A Netsukefile that needs a field out of JSON, a layered configuration, a build matrix, or a version check reaches for shell() today. RFC 0006 proposes absorbing those operations into the template language as pure, bounded, capability-classified helpers, so the generated graph stays byte-for-byte reproducible and netsuke help targets can answer more questions without executing anything.

Status on 19 October 2026

  • Not in v0.1.0-beta4. No helper from the expansion ships. All 49 tasks in roadmap phase 6 are unticked. The shipped standard library is documented separately.
  • Specified. RFC 0006 (status Proposed, created 26 August 2026) was merged to main as a specification. Its release target: after v0.1.0 final, and “no part of this RFC may land in the v0.1.0 hardening release”. It names v0.1.x and v0.2.x as targets.
  • Being split. Open PR #697 divides the accepted set among focused child RFCs 0013 to 0020, of which only RFC 0013, structured data interchange, is written; it adds a test suite asserting that every accepted helper has exactly one owner.
  • Adjacent work in flight. Open PR #702 (roadmap 3.14.8) implements env(name, default=...) and a compact filter. shell_escape and shell_join, on which RFC 0006's shell_quote depends, are not on that branch yet.

Why expand it

Fifty-seven new helpers — forty-one filters and sixteen tests — across ten capability groups, surveyed from ansible-core and filtered hard. The RFC's own case for the volume: “the alternative is not a smaller standard library; it is the same operations expressed as shell pipelines that Netsuke can neither audit nor reproduce.”

Measured by subprocesses removed

The roadmap states the value plainly: it is “measured by the manifest operations that no longer require a subprocess, not by name parity with Ansible”.

Stolen one at a time

Ansible's generic lookup(...) dispatcher is rejected; useful capabilities come across as typed, named helpers. One name per capability: no alias families, no win_* variants where a dialect argument will do.

Mostly pure

Fifty-two of the fifty-seven are pure. Four read the filesystem and one reads the environment. None reads the clock, the network, or a subprocess, so most of the new surface is also available to netsuke help targets.

The helper contract

RFC 0006 §6 is normative: “A child issue that does not satisfy every clause below for every helper it adds is not complete.” Roadmap step 6.1 builds the shared machinery before any family lands.

Clause What it requires
6.1 Purity classesExactly one label per helper: pure, or clock-, environment-, filesystem-, network-, or subprocess-observing. Only pure helpers are available in manifest queries.
6.2 Query availabilityEvery non-pure helper registers in the query environment as an always-failing stub with a localized diagnostic — “they must never simply be absent”. A test exercises each registration.
6.3 Determinism“A Netsukefile compiled twice from the same inputs must produce the same generated Ninja byte for byte.” No hash-table ordering, LF everywhere, dialect-driven separators, C locale, no unseeded randomness.
6.4 Capability boundaryFilesystem through the injected cap_std handle, environment through an injected reader. “A path outside the capability boundary is an error, never a false result.”
6.5 Platform contractUnix, Windows, and unsupported-platform behaviour documented. Lexical helpers take dialect: host, posix, or windows. A helper that cannot honour its contract fails with a typed diagnostic naming the platform and the capability.
6.6 Types and errorsEnumerated accepted kinds, no silent coercion, strict undefined, none as a value, duplicates rejected by default, checked overflow, and unknown option values that list the valid set.
6.7 Canonical equalityValues are equal when their RFC 8785 canonical JSON is byte-identical. Deduplication keeps first appearance.
6.8 Resource boundsRejects unreasonable expansion before allocating: 8 MiB input and depth 128 for parsers, 100,000 YAML alias nodes, 100,000 tuples for product and combinations, 10,000 for permutations, 1 MiB compiled patterns.
6.9 DiagnosticsA domain error enum per group; Fluent keys under stdlib.<module>.<condition>; machine codes of the form netsuke::jinja::<module>::<reason>.
6.10 Naming“Netsuke registers exactly one name per capability.”
6.11 Docs and testsA guide entry with its purity label, an executed tested-example, unit, property, and integration tests, a query-disposition test, and an inventory row.

Where host probing comes up — the filesystem predicates and expandvars of roadmap 6.7 — the separate system facts preview (RFC 0029) covers what a manifest can learn about its host without probing at all.

Helper families

Shallow end first. Each panel gives signatures exactly as the RFC heads them, and each family links its roadmap step. Every one is proposed.

Read structured data without a subprocess

Roadmap 6.2

Duplicate keys are rejected with the key and byte offset: “last-key-wins is a silent data-loss trapdoor in a build manifest”. to_yaml quotes yes, no, on, and off so the Norway problem cannot reach a generated file. Specified in detail by child RFC 0013.

text | from_json
text | from_yaml_all
value | to_yaml(indent=2, sort_keys=false, explicit_start=false)

Make configuration layering expressible

Roadmap 6.3

combine keeps first-appearance key order and offers replace, keep, append, and prepend list policies. The RFC states outright that recursive merging is not associative, with a worked counterexample.

mapping | combine(*others, recursive=false, list_merge='replace')
sequence | items2dict(key_name='key', value_name='value', duplicates='error')

Build matrices with stable ordering

Roadmap 6.4

product is “for build matrices over target triples, feature sets, and optimization levels”. Cardinality is checked before allocation, and the roadmap test deliberately varies hash state to catch any helper reimplemented over a hash set.

a | product(*others, repeat=1)
{{ records | selectattr('tags', 'contains', 'rust') | list }}

Text and version conditions

Roadmap 6.5

One regular-expression dialect, netsuke-regex-v1, over the Rust regex crate. Version comparison is strict SemVer with a required operator: “1.82 is not a Semantic Version and is rejected”. Whether to tolerate a v prefix is an open question.

value | regex_replace(pattern, replacement, ignorecase=false, multiline=false, count=0, mandatory_count=0)
{% if compiler_version is version('1.82.0', '>=') %}

Path text for a platform other than the host

Roadmap 6.6

Python lets ['/safe/root', '/etc/passwd'] | path_join yield /etc/passwd; “that is a trapdoor in a build manifest and Netsuke refuses it”. A Unix host must parse a Windows path exactly as a Windows host would.

parts | path_join(dialect='host')
path | relpath(start, dialect='host')

Probe host state through query-gated seams

Roadmap 6.7

The four filesystem-observing and one environment-observing helpers of the set. All run through injected seams and are refused under help targets; mount may ship Unix-only, with an explicit error on Windows rather than a silent false.

path is link_exists
text | expandvars(dialect='host', missing='error')

File text and stable identities

Roadmap 6.8

to_uuid is UUIDv5 under a frozen Netsuke namespace. A block comment whose text contains the closing marker is an error, so comment text cannot escape into the generated file as live syntax. human_readable and human_to_bytes are locale-independent.

text | to_uuid(namespace=<Netsuke namespace>)
text | comment(style='hash', prefix=none)

Timestamps without the clock

Roadmap 6.9

Parsing and formatting are pure and never read the clock. A closed specifier set, the C locale, and no zone database: %Z is refused and IANA zone names are rejected. Identical manifests produce identical text on machines with different locales.

text | to_datetime(format='%Y-%m-%d %H:%M:%S', timezone='UTC')
timestamp | strftime(format)

Deliberately deferred. Seeded shuffle and random, log/pow/root, and type_debug wait on roadmap 6.10, which adopts a candidate only with a named consumer. Unseeded randomness stays rejected permanently.

Proportionate quality gates

Roadmap step 6.11 sits outside RFC 0006's Ansible-derived set but follows the same contract. It exposes the paths changed between two commits as typed data, and a pure filter to test them, so a manifest can leave an expensive gate out of the graph when nothing it checks has changed. The design is Proposed; ADR-015 governs the bounded Git adapter.

The design document's motivating example (proposed)

targets:
  - name: lint-rust
    when: >-
      git_changed_files('origin/main...HEAD')
      | matches_glob('**/*.rs', 'Cargo.toml', 'Cargo.lock')
    command: cargo clippy --workspace --all-targets --all-features -- -D warnings

git_changed_files(range)

Accepts exactly A..B and A...B, rejecting malformed and option-like endpoints before Git starts. Runs fixed argument vectors with --no-lazy-fetch, parses NUL-delimited output, and returns sorted, de-duplicated paths. Impure: it flips the render's impurity flag before Git runs and is a named failing stub under help targets.

matches_glob(*patterns)

Pure and variadic: true when any path matches any pattern, with semantics aligned to glob(). At most 64 patterns and 65,536 pattern bytes, checked before anything is compiled. Every path is validated even after an early match.

Scope is narrow on purpose: no working-tree inspection, no untracked files, and no per-linter file selection. The helper decides whether a target enters the graph; it does not change Ninja's incremental model. The roadmap's success test: the documented example omits its Rust target for a Python-only changeset and includes it for every Rust path class.

From the shallow end

Every new helper is additive. A manifest that uses none of them compiles as it does today, and the existing basename and dirname gain dialect as an optional argument rather than changing behaviour. The route in, in order:

  1. Today. The shipped standard library — path filters, glob, which, command_available, and collection filters — covers most manifests.
  2. First step. Replace one shell('jq ...') with from_json, or one --version scrape with the version test. Both are pure, so help targets can evaluate them.
  3. Next. Layer configuration with combine and expand matrices with product, trusting the ordering to be stable.
  4. Cross-platform. Compose paths with an explicit dialect, and read the host's own conventions from the host facts of RFC 0029 once they land.
  5. Deep end. Probe the host through the 6.7 predicates, and gate expensive checks on a Git changeset.

Confidence

All planned. The phase's success criterion is written as tests, not prose.

Byte-identical output, tested end to end

A representative matrix manifest must compile twice to byte-identical Ninja. The layering step's example must do the same in an isolated workspace.

Property tests where laws exist

Round trips for encoders and serializers, ordering and idempotence for set algebra, composition laws for combine, path_join, and normpath.

Query disposition cannot be forgotten

Roadmap 6.1.6's contract test makes it impossible to add a non-pure helper without deciding how it behaves under help targets.

Documentation that runs

Every guide example is executed by the existing documentation-example tests, and the maintained inventory is test-enforced so it cannot drift. PR #697's coverage suite proves each accepted helper has exactly one owning child RFC.

Accessibility and localization

  • Every message is a Fluent key. Netsuke ships 35 locales, and the new diagnostics join that set.
  • Codes survive translation. Each error carries a stable netsuke::jinja::<module>::<reason> code, the shape netsuke::jinja::which::not_found already uses. #702 puts the code in the Fluent text so the diagnostic stays greppable in every locale.
  • A gap, named. The query-restriction message is built with format! and is not localized today. Roadmap 6.1.5 fixes it.
  • Output that ignores the reader's locale. Pinning the C locale and ASCII digits is a determinism rule, and it also means a build log reads the same wherever it is read.
  • Where the design is silent. The RFC says nothing about screen-reader-specific rendering of helper diagnostics beyond what the shared reporter already provides.