- Forthcoming
- Reusable Modules
Reusable Modules
Reusing a Netsukefile has two routes today: copy it into the next repository and watch the copies drift, or push the work into shell scripts and lose the graph. “Reusable modules” is this site’s umbrella for what Netsuke’s design calls manifest composition: repository-relative includes, versioned local bundles, and digest-pinned external bundles. One rule runs through all three — composition order is explicit, stable, and incapable of silently replacing a declaration.
Status on 19 October 2026
- Not in v0.1.0-beta4. Nothing is implemented. The released manifest rejects unknown top-level keys, so an
includes:orbundles:key is an error today. Every task in phases 16 to 19 of the composition roadmap is unticked. - Proposed. RFC 0002, RFC 0003, and RFC 0004 were written on 26 August 2026 and merged as documents in pull request #600. They stay proposed until the roadmap’s explicit acceptance tasks complete, and some details below are still open.
- Preview targeted at v0.2.0 — the local layers. Issue #593 says v0.2.0 should at minimum accept the whole RFC sequence and implement the local composition foundation. External Git acquisition may be staged later if its transport, cache, and verification work would delay the local spine. The roadmap promises no dates.
- Undecided. Who writes and maintains the bundles people will actually use is open in issue #784: nobody has committed to shipping official bundles. The lock file’s name and schema are also undecided.
Why composition, and why three layers
The only reuse a manifest has today is the macros key: named Jinja macros
registered before other fields render, visible within that one file. Macros cut repetition inside a manifest; they draw no
source-file boundaries. Meanwhile the same groups recur across repositories — Rust formatting, Clippy, rustdoc, and tests;
Python formatting, linting, type checks, and tests; release, audit, spelling, and documentation gates; feature-lane matrices.
Jinja’s own include and import were considered and rejected: they evaluate too late, mix source
composition with value rendering, and could make a metadata query execute build-time helpers.
Includes · RFC 0002
Split one large Netsukefile into reviewable local files, with optional namespaces, without changing the graph and without letting file order decide which declaration wins.
Local bundles · RFC 0003
Package a quality gate with a version, typed parameters, and a public surface. Copying fragments between repositories loses version identity; a bundle keeps it.
External bundles · RFC 0004
Consume a reviewed upstream release from Git instead of vendoring each update, without trusting a movable tag or touching the network during a build.
Each layer is useful alone, and the order is normative: local includes first, local bundles second, networked acquisition
only later. None of the three is required; a manifest with no includes behaves exactly as it does now.
Repository-relative includes
An include names a fragment by a literal path relative to the including file. An optional as puts the
fragment’s named rules, targets, and actions in a namespace, so lint in the fragment below becomes
rust_quality::lint; references inside the fragment are rewritten before cross-fragment resolution. An unnamespaced
include contributes its declarations directly and is correspondingly more likely to collide.
Proposed syntax, RFC 0002 §§5–5.1
includes:
- path: build/netsuke/release.yaml
- path: build/netsuke/rust-quality.yaml
as: rust_quality
Illustrative metadata fields, RFC 0002 §11
{
"name": "rust_quality::lint",
"source": "build/netsuke/rust-quality.yaml",
"namespace": "rust_quality"
}
| Question | Proposed answer |
|---|---|
| In what order? | Depth-first post-order. For a root R including A then B, where A includes C, the order is C, A, B, R — never directory iteration, hash-map, or filesystem metadata order. |
| Does a later file win? | No. Order does not imply replacement. A duplicate name is an error naming both sources, their spans, and their include chains, with a remedy: add as, rename one, or extract a bundle. Variables are never deep-merged. |
| Cycles and repeats? | A cycle is an error that prints the chain; the same file included twice, even through another spelling or a symlink, is an error. |
| Which paths? | Literal strings opened through the workspace capability. Absolute paths, workspace escapes, globs, Jinja, env(), glob(), and network sources are rejected. |
| What does discovery run? | Nothing. netsuke help targets and JSON metadata resolve and parse includes, and show each declaration’s origin, but never render or execute recipe fields. |
Roadmap: phase 16 — 16.1 loading each unit once, 16.2 preserving declaration meaning, and 16.3 builds and metadata queries. Task 16.1.1 still has to settle how a namespaced fragment’s variables are projected; the RFC requires only that it stay a typed namespace.
Versioned local bundles
A bundle is a directory with a strictly parsed descriptor, NetsukeBundle.yaml, beside its own manifest and
fragments. The descriptor is not a build manifest: it names the bundle, versions it, lists every runtime resource its recipes
use, declares typed parameters, and lists exports. Everything not exported is private. Consumers cannot reach past an exported
entry point to the declarations behind it, and a bundle cannot read the importer’s unrelated variables.
Bundle descriptor, trimmed from RFC 0003 §4
bundle:
name: df12.rust-quality
version: 1.4.2
manifest: Netsukefile.bundle.yaml
runtime_resources: []
parameters:
features:
type: sequence<string>
default: []
exports:
actions:
- lint
- test
Proposed import, RFC 0003 §5.1
bundles:
- name: df12.rust-quality
source:
path: build/bundles/rust-quality
version: "=1.4.2"
as: rust
with:
cargo: cargo
features:
- serde
- tracing
The df12.rust-quality name is the RFC’s worked example, not a published bundle. Exported lint
under as: rust becomes rust::lint, and the same bundle may be instantiated twice under distinct namespaces.
Typed parameters
string, bool, integer, path, sequence<string>, sequence<path>, and mapping<string, string>. No default means required; an unknown parameter is an error; values are validated before any Jinja evaluates, then read inside the bundle as bundle.params. The same contract is meant to serve root manifest inputs — see Typed Inputs, which forbids a rival parameter system.
Catalogue selection
Instead of source.path, a source.catalogue directory holds candidate versions, and a requirement such as "^1.4" selects the highest satisfying version. Directory names are hints only; malformed candidates fail closed; a tie is an ambiguity error. Pre-release ordering is not settled: the RFC’s prose and its own example disagree, and task 17.1.1 must reconcile them.
Content digests and locks
A SHA-256 digest covers every graph-reachable file and declared resource, by bundle-relative path, mode, and exact bytes — not timestamps, owners, or .git — so relocating a bundle keeps its digest. A lock record binds the selection; CI and release builds require it to match, and editing files without bumping the version shows up as a digest mismatch.
Still open. The lock file’s name and schema, the canonical tree and parameter encodings, and the
command spellings. RFC 0003 sketches conceptual bundle subcommands for listing, inspecting, updating, and
verifying, but its inspect must first be reconciled with the CLI’s canonical get, so none is
shown here as final. Local resolution never consults Git: the same files resolve the same way whether or not .git is present.
A bundle’s recipes keep exactly the execution capabilities of ordinary recipes; provenance does not make an unsafe
command safe. Task 17.4.4 joins bundles to the structured-command track:
process directories stay anchored to the importing workspace rather than the fragment or cache, and bundle content may not add
shell definitions or replace a resolved shell through PATH, bindings, or placement.
Roadmap: phase 17 — 17.1 identity and parameters, 17.2 private instantiation, 17.3 content and locks, and 17.4 validation and the execution boundary.
Digest-pinned external bundles
May land after v0.2.0. RFC 0004 may be accepted during v0.2.0 design work, but external acquisition itself may ship later than the local layers. Phase 18 starts only once includes and local bundles are stable.
An external source names a Git URL, exactly one of a tag or a full commit, an optional
subdir, and a mandatory digest. A tag is a discovery handle, not the trust anchor: the lock record and
the canonical content digest bind the bytes. Branches, HEAD, default branches, abbreviated commit IDs, and
version-only references are rejected, and a tag that later resolves to a different object fails verification even when the
bundle digest is unchanged — retagging is treated as a provenance event, not shrugged off.
Proposed syntax, RFC 0004 §5 (with omitted)
bundles:
- source:
git:
url: https://github.com/df12/netsuke-bundles.git
tag: rust-quality/v1.4.2
subdir: bundles/rust-quality
digest: sha256:4e9f2d...
name: df12.rust-quality
version: "=1.4.2"
as: rust
Provenance output, RFC 0004 §15
rust: df12.rust-quality 1.4.2
git tag rust-quality/v1.4.2
commit 8c5e4e6d76d75de67f66f09cb2c63d69d2e14f84
digest sha256:4e9f2d...
lock verified, cache hit
The df12/netsuke-bundles repository is illustrative, taken from the RFC’s examples. It does not exist as a
published source, and whether any official bundles will is the open question in issue #784.
One command touches the network
Builds and metadata queries, netsuke help targets included, are cache-only in every mode: they never fetch, never publish a cache entry, and never update a lock. netsuke bundle fetch is the sole command allowed to acquire, and it changes a lock only when given --update-lock. A cache miss is a typed diagnostic naming the namespace and expected digest.
Hardened acquisition
No shell pipelines; Git hooks, filters, submodules, and LFS smudging disabled; credentials in URLs rejected and redacted everywhere; bounded transfers. Cache entries are content-addressed, written atomically, and re-verified on every use. Signed tags would be additive, never a substitute for the digest, and their policy is deferred to phase 19.
Roadmap: phase 18 — 18.1 the acquisition boundary, 18.2 exact Git objects, 18.3 cached provenance, and 18.4 offline reproduction; then 19.1, which decides extensions such as signed-tag policy only against recorded consumer needs.
From the shallow end
A single Netsukefile stays a complete, supported manifest. Manifests without includes keep their current
behaviour, and the new keys arrive behind an additive manifest-format minor version, so an older Netsuke rejects them cleanly
rather than misreading them. Each step below is optional.
-
1. Split the monolith into includes
Move one group into a fragment, add a literal include, add
asif names collide, update references, and compare graph and help snapshots before deleting the originals. Because duplicates are errors, the copied and extracted versions cannot both stay active by accident. -
2. Turn a fragment into a local bundle
Add a descriptor, give it a name and version, replace reads of importer variables with declared parameters, list the exports, import it under a namespace, and generate the lock record.
-
3. Swap a vendored bundle for an external one
Publish it unchanged under a reviewed tag, compute its digest, switch the source to
gitwhile keeping namespace, parameters, identity, and version, review the external lock, and prove graph equivalence before removing the vendored copy.
Includes never gain version semantics, and neither includes nor local bundles gain network behaviour. The roadmap is explicit that phase numbers identify work, not a requirement to finish every earlier phase, so a project may stop after any step.
Confidence
None of what follows exists yet; these are the guarantees the RFCs promise and the evidence the roadmap demands before any task is ticked.
Designed guarantees
- Deterministic order. Traversal and catalogue selection are independent of enumeration order.
- No silent override. Duplicates, cycles, and repeated files are errors with full include chains.
- Exact bytes. The build fingerprint covers every included byte; touching metadata alone changes nothing. Bundle and external digests bind content; tag and commit drift are detected.
- Discovery runs nothing. Instrumented metadata queries must record zero command, recipe-only helper, and network invocations.
- Honest diagnostics. Errors point at the original fragment spans, not a synthetic merged file.
Planned tests and proofs
- Property tests generating bounded acyclic and cyclic include graphs, version catalogues, and parameter maps.
- Kani harnesses may check the bounded traversal and that every composition unit is emitted exactly once; the roadmap requires them to cover the production state machine, not a disconnected model.
- Git tests for annotated and lightweight tags, retagging, and limits, run against a local protocol server rather than the public internet.
- Canaries: split a real monolithic Netsukefile and prove graph equivalence; consume one quality-gate bundle from at least two downstream repositories; build a tagged bundle from a verified cache with the network disabled. Unix-like and Windows runs.
The downstream migrations behind those canaries are tracked in issue #598.
Accessibility & localization
Every human-readable message Netsuke emits is localized by default through its Fluent catalogues, so include cycles, bundle resolution failures, lock mismatches, and provenance summaries arrive in the reader's locale like any other diagnostic; bundle names, versions, digests, and error codes stay untranslated so they can be matched. The composition documents add no composition-specific output modes, and this preview does not invent any. What else follows from them:
- Bundle commands join the existing CLI vocabulary. The roadmap requires that vocabulary to be extended before any bundle command is built, so bundle commands inherit the global flags v0.1.0-beta4 already has —
--json,--locale,--color,--emoji,--progress, and--accessibility— rather than inventing their own. See the CLI reference. - Human and JSON output. Provenance and failures are specified for both, with human output kept concise and failures distinguishable by type rather than by parsing prose.
- Redaction by default. Bundle parameter values are redacted in diagnostics and metadata unless declared
expose: non-secret; credentials in Git URLs are rejected and redacted; absolute host and cache paths never appear in metadata.
The translation guide covers how those messages are translated, and the glossary keeps terms such as bundle and include consistent across locales.