Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
a9fe64c
Draft the 7.1.2 options-carrying loader ExecPlan
Sep 26, 2026
0070023
Fix spelling and formatting in the 7.1.2 ExecPlan
Sep 26, 2026
59f3b02
Revise the 7.1.2 ExecPlan after expert-panel review
Sep 26, 2026
01f4831
Extend roadmap 7.4.1 to sandbox the file-reading filters
Sep 27, 2026
679b194
Reconcile the 7.1.2 ExecPlan with the rebased base
Oct 14, 2026
aee88ae
Record the Stage A baseline gate results in the plan
Oct 14, 2026
8e3b1c2
Rename three plan module paths to satisfy the layout contract
Oct 14, 2026
07a619e
Record the manifest loader characterization baseline
Oct 14, 2026
e6807a6
Add the missing glob row to the query-refusal baseline
Oct 14, 2026
36910a2
Repair the lints the characterization suites introduced
Oct 15, 2026
8b146e8
Diagram the loader's registration modes
Oct 15, 2026
7d11b56
Record the EP-M0 gate repair in the plan
Oct 15, 2026
5ff29b6
Extract the loader seam predicates the branch lint demands
Oct 15, 2026
fd3d446
Record the EP-M0 gate results and repair the stale log citation
Oct 15, 2026
cd6199a
Record the EP-M0 re-run and correct three plan inaccuracies
Oct 15, 2026
3be6665
Correct the layout rationale and record the EP-M1 recon
Oct 15, 2026
3dbdade
Correct the plan-touching gate set
Oct 15, 2026
49c3cd0
Match either path separator in the temp-dir snapshot filter
Oct 15, 2026
b8e2014
Make the characterization rows record what they claim
Oct 15, 2026
4022b1f
Correct two false claims in the test-framework design
Oct 15, 2026
5d6d7f9
Point the closure step at ADR-055, not the spent ADR-041
Oct 15, 2026
aaa2815
Repair three failures the gate batch surfaced
Oct 15, 2026
5d526f5
Reconcile the ExecPlan with the loader design and record the repair b…
Oct 15, 2026
6b6fcdb
Normalize JSON-escaped temporary paths in snapshot filters
Oct 16, 2026
c5d8680
Record the milestone review and the filter repair batch
Oct 16, 2026
8e1f680
Share the temporary-path snapshot filter
Oct 16, 2026
a047e93
Record the C12 baseline and the nine-gate batch
Oct 16, 2026
f961145
Correct the loader mode diagram against its own prose
Oct 16, 2026
856b437
Record the batch D gates and fix a stale tense
Oct 16, 2026
8f6bd2e
Approve the manifest loader plan
Oct 25, 2026
132f4b6
Record the manifest loader rebase audit
Oct 25, 2026
d9e0390
Extract the restricted-helper surface inventory
Oct 30, 2026
c458cfd
Move query classification into its own module
Oct 30, 2026
7dfe4ed
Record EP-M1 as complete
Oct 30, 2026
adb0388
Repair the EP-M1 milestone review findings
Oct 30, 2026
37add3b
Use underscore emphasis in the design-document status note
Oct 31, 2026
b26a132
Record the EP-M1 review outcome and gate evidence
Oct 31, 2026
cc17f13
Add options-carrying manifest loader entry point
Oct 31, 2026
0f5691a
Record EP-M2 implementation and its OBL-1 evidence
Oct 31, 2026
d3440fd
Add loader options to the Kani proof scope
Oct 31, 2026
dc630bf
Record the EP-M2 gate batch and close the milestone
Oct 31, 2026
fee5b4a
Record the EP-M2 review quota blocker and publication
Oct 31, 2026
3657fe9
Add the test-mode stdlib registration
Oct 31, 2026
b40d4a2
Record EP-M3 implementation and its evidence
Oct 31, 2026
04e7eae
Split manifest keys into their own module
Oct 31, 2026
9ce672b
Split the EP-M3 test modules under the line cap
Oct 31, 2026
0d508c9
Correct the plan's spelling of organization-wide
Oct 31, 2026
f8b71cc
Discharge the EP-M3 workflow contract obligations
Nov 1, 2026
443f5c7
Record the EP-M3 gate batch and close the milestone
Nov 1, 2026
86ed692
Repair the EP-M3 milestone review findings
Nov 1, 2026
079b3c5
Re-wrap the EP-M3 review note to the gate width
Nov 1, 2026
cddb21f
Add template overlays to the test-mode manifest loader
Nov 1, 2026
f2c88af
Record the EP-M4 gate batch and the ADR serial deviation
Nov 1, 2026
64d08b8
Document the 7.1.2 loader modes and test surface
Nov 1, 2026
df773c0
Repair EP-M4 lint finding and two doc typos
Nov 1, 2026
0dfe544
Record the EP-M4 lint repair and the green nine-gate batch
Nov 1, 2026
e3b292e
Repair the rebased localization key union
Nov 1, 2026
2f06108
Record the rebase onto main in the 7.1.2 plan
Nov 1, 2026
f31ee39
Reflow the rebase-resolved documentation paragraphs
Nov 1, 2026
000d3a5
Record the nine-gate batch for the rebased 7.1.2 candidate
Nov 1, 2026
c3b2617
Move manifest string loaders beside their peers
Nov 1, 2026
5c13d55
Cover the new loader module in the Kani proof scope
Nov 1, 2026
3146c33
Compare registrations by helper kind in the surface scan
Nov 2, 2026
a76def2
Correct stale loader documentation and spelling
Nov 2, 2026
4c1879f
Correct the ExecPlan's ADR serial references
Nov 2, 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
//! A build with the `lint` feature writes both into a `lint/` subdirectory
//! instead, because its command tree has a `check` subcommand the release
//! artefacts must not carry.
//! - Audit localization keys declared across the three key tables —
//! `src/localization/keys.rs`, `src/localization/check_keys.rs`, and
//! `src/localization/clap_error_keys.rs` — against the Fluent bundles in
//! - Audit localization keys declared across the four key tables —
//! `src/localization/keys.rs`, `src/localization/check_keys.rs`,
//! `src/localization/clap_error_keys.rs`, and
//! `src/localization/manifest_keys.rs` — against the Fluent bundles in
//! `locales/*/messages.ftl`, failing the build if any declared key is missing
//! from a locale.
use cap_std::{ambient_authority, fs::Dir};
Expand Down Expand Up @@ -85,6 +86,7 @@ mod host_pattern;
pub mod locale_catalogues;

mod build_l10n_audit;

/// Message rendering, shared with the library crate.
///
/// Exposed as `crate::localization`, which `cli`, `cli::l10n`, and
Expand Down Expand Up @@ -210,6 +212,7 @@ fn emit_rerun_directives() {
println!("cargo:rerun-if-changed=src/localization/keys.rs");
println!("cargo:rerun-if-changed=src/localization/check_keys.rs");
println!("cargo:rerun-if-changed=src/localization/clap_error_keys.rs");
println!("cargo:rerun-if-changed=src/localization/manifest_keys.rs");
println!("cargo:rerun-if-changed=src/locale/catalogues.rs");
println!("cargo:rerun-if-changed=Cargo.toml");
// The locale registry owns the catalogue list, so the rerun directives are
Expand Down Expand Up @@ -312,6 +315,7 @@ fn generate_completions(out_dir: &Path) -> Result<(), Box<dyn std::error::Error>
);
Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
emit_rerun_directives();
build_l10n_audit::audit_localization_keys()?;
Expand Down
23 changes: 15 additions & 8 deletions build_l10n_audit/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! Localization audit for the build script.
//!
//! Compares the keys declared by `define_keys!` in the three key tables —
//! `src/localization/keys.rs`, `src/localization/check_keys.rs`, and
//! `src/localization/clap_error_keys.rs` — with every catalogue named by the
//! locale registry in `src/locale_catalogues.rs`. The three files are one key
//! set: `keys.rs` re-exports the other two, and a key declared in more than one
//! table is an error. The audit fails the build when a catalogue is missing a
//! Compares the keys declared by `define_keys!` in the four key tables —
//! `src/localization/keys.rs`, `src/localization/check_keys.rs`,
//! `src/localization/clap_error_keys.rs`, and
//! `src/localization/manifest_keys.rs` — with every catalogue named by the
//! locale registry in `src/locale_catalogues.rs`. The four files are one key
//! set: `keys.rs` re-exports the other three, and a key declared in more than
//! one table is an error. The audit fails the build when a catalogue is missing a
//! declared key, carries an orphaned key, or interpolates a different set of
//! variables from the English source for a shared message.
//!
Expand All @@ -14,8 +15,11 @@
//! than being a second source of truth.

mod compare;

mod ftl;

mod keys;

mod metadata;

use crate::locale_catalogues::{LocaleCatalogue, SOURCE_LOCALE, SUPPORTED_LOCALES};
Expand All @@ -28,12 +32,15 @@ use std::path::{Path, PathBuf};
use std::{error::Error, io};

/// Repository-relative paths of the source files that define the localization
/// keys. `keys.rs` re-exports the other two, so together they are one key set.
const KEY_SOURCES: [&str; 3] = [
/// keys. `keys.rs` re-exports `check_keys.rs`, `clap_error_keys.rs`, and
/// `manifest_keys.rs`, so together they are one key set.
const KEY_SOURCES: [&str; 4] = [
"src/localization/keys.rs",
"src/localization/check_keys.rs",
"src/localization/clap_error_keys.rs",
"src/localization/manifest_keys.rs",
];

/// Repository-relative path of the manifest whose locale metadata is audited.
const CARGO_MANIFEST: &str = "Cargo.toml";

Expand Down
40 changes: 31 additions & 9 deletions docs/adrs/adr-0050-bound-manifest-template-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ MiB source, 10,000 `foreach` values, and 50,000 expanded entries. Exhaustion
uses a localized, redacted diagnostic and closed-vocabulary telemetry labels.
The budget types contain only accounting data; the Jinja adapter owns
localization and MiniJinja error conversion, and the normal full-load boundary
owns exhaustion telemetry. Manifest-query loading selects
`ManifestLoadMode::ManifestQuery`, which suppresses this budget-exhaustion
metric at the loading boundary; its omitted expansion-report observer controls
the separate expansion metrics. Other instrumentation remains owned by the
boundaries that invoke it. Budget values from the project file and every file
in its `extends` chain may only narrow ceilings established by defaults,
trusted configuration, the environment, or CLI flags. Malformed, non-positive,
and unrepresentable budget values are rejected during configuration merge
rather than treated as absent.
owns exhaustion telemetry. Manifest-query loading selects the `ManifestQuery`
stdlib registration, which suppresses this budget-exhaustion metric at the
loading boundary; its omitted expansion-report observer controls the separate
expansion metrics. Other instrumentation remains owned by the boundaries that
invoke it. Budget values from the project file and every file in its `extends`
chain may only narrow ceilings established by defaults, trusted configuration,
the environment, or CLI flags. Malformed, non-positive, and unrepresentable
budget values are rejected during configuration merge rather than treated as
absent.

After a successful full manifest load, the runner continues to call
`record_manifest_structure` for the bounded fixed-vocabulary structural counts
Expand Down Expand Up @@ -237,6 +237,28 @@ macro output or internal macro and capture buffering. Addendum B's deferred
remediation and its MiniJinja 3.0.0-final release condition therefore remain
unchanged.

## Addendum E: Telemetry scope for derived loads (2026-11-01)

Expansion and budget telemetry describes normal manifest loading: how often
`foreach`/`when` evaluated, what the resource budget observed, and how a build
manifest's evaluation behaved. Two derived load modes measure something else
and must not contribute to those counters.

`ManifestQuery` loads inspect discovery metadata and evaluate a manifest only
far enough to answer a query. `Test` loads evaluate a manifest under a
deny-by-default stdlib surface. Counting either as a build-shaped load would
distort usage evidence and blend a query or test outcome into statistics about
builds.

The load's registration therefore decides:
`StdlibRegistration::records_expansion_and_budget_telemetry` is true for
`AmbientFull` and `Full` and false for `ManifestQuery` and `Test`. The loader
passes that decision to the budget adapter and skips the expansion telemetry,
so a query or test load still enforces the resource ceilings but records
nothing under the build's telemetry names. This narrows the counters' scope; it
does not change the budget itself or the guarantee this record's Addenda A–D
describe.

## References

- [#651](https://github.com/leynos/netsuke/issues/651) and
Expand Down
13 changes: 7 additions & 6 deletions docs/adrs/adr-0056-manifest-linting-under-netsuke-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,13 @@ Gated behind `#[cfg(feature = "lint")]`: `crate::lint`; the CLI's
the runner's `check` modules and dispatch, the lint `RunnerError` variants, and
check telemetry; and the `source` and `name` fields of the internal
`LoadedManifest`. Deliberately not gated: the Fluent message keys in
`src/localization/keys.rs`, `src/localization/check_keys.rs`, and
`src/localization/clap_error_keys.rs` and their catalogue entries, because the
build-time localization audit is bidirectional across all 35 catalogues and
gating the constants would make every locale look orphaned; and the
subcommand-to-key routing in `src/cli/l10n/mod.rs`, which only maps command
names and carries no linting logic.
`src/localization/keys.rs`, `src/localization/check_keys.rs`,
`src/localization/clap_error_keys.rs`, and `src/localization/manifest_keys.rs`
and their catalogue entries, because the build-time localization audit is
bidirectional across all 35 catalogues and gating the constants would make
every locale look orphaned; and the subcommand-to-key routing in
`src/cli/l10n/mod.rs`, which only maps command names and carries no linting
logic.

Both configurations are verified in continuous integration (CI): the existing
gates build and test with `--all-features`, and a second, uninstrumented CI
Expand Down
201 changes: 201 additions & 0 deletions docs/adrs/adr-0070-deny-by-default-test-mode-stdlib.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Architectural decision record (ADR) 0070: Deny-by-default test-mode stdlib

## Status

Accepted on 2026-11-01. The `netsuke test` manifest loader grants a
deny-by-default stdlib surface, and template overlays replace withheld
functions by name.

## Date

2026-11-01

## Context and Problem Statement

Roadmap item 7.1.2 makes the manifest loader options-carrying so that a test
runner can load a manifest without granting the capabilities a build needs. The
technical design (§4.2, §5.5) makes three requirements binding:

- A test load must not read the host environment, inspect or mutate the project
tree, run a command, reach the network, or depend on the wall clock. An
unmocked impure call is an error, not a host-dependent answer (constraints
C2, C3, C4).
- A withheld helper must fail with a located diagnostic naming the operation.
An absent helper yields MiniJinja's generic unknown-name error, which reads
as a typo in the manifest.
- A test author must still be able to test a manifest that calls an impure
helper, by supplying the answer a real run would have produced without
granting the helper itself.

The loader already selects its stdlib boundary through a crate-private
`StdlibRegistration` enum with `AmbientFull`, `Full`, and `ManifestQuery`
variants. The question is how to add a fourth mode without duplicating the
capability decision, the refusal diagnostics, or the environment seam.

## Decision Drivers

- One place decides every capability for a load (constraint C5).
- Refusals are localized, carry a stable diagnostic code, and expose a typed
error source so a caller can distinguish a withheld capability from an
unknown name.
- The test surface is deterministic: it depends only on inputs the test
supplied.
- The overlay mechanism is a loader capability compiled into the shipped
binary, not a test-only `cfg` path (constraint C9).
- Every helper whose availability differs by load surface is inventoried once,
and a new helper cannot be registered without stating what each surface does
with it.
- No second mode enum, and no public load-mode vocabulary that an embedder
could widen.

## Decision Outcome

Extend `StdlibRegistration` with `Test(Box<TestSurface>)` and keep the enum
crate-private. `TestSurface` carries three things: the `StdlibConfig` the
surface grants (including the clock `now()` may read), an `EnvReader` that
`env()` answers from, and the `TemplateOverlays` the test supplied. Keeping the
payload in the surface rather than in the load options means the enum never
grows a second capability field.

Availability is decided by one inventory. `SURFACE`
(`src/stdlib/restricted_helpers/mod.rs`) records a query disposition and a test
disposition beside every helper whose availability depends on the load surface.
`register_for_test` registers exactly the refused rows as stubs, and a contract
test checks the inventory in both directions. The implemented surface is:

- Available: `env()` over the surface's injected reader; `timedelta`; the
lexical path filters `basename`, `dirname`, `with_suffix`, `relative_to`; the
recipe-text quoting helpers `shell_quote`, `shell_join`; the collection
filters `uniq`, `flatten`, `compact`, `group_by`; and the legacy Boolean
render formatter.
- Refused: the functions `glob`, `fetch`, `which`, and `command_available`;
the filters `which`, `shell`, `grep`, `contents`, `realpath`, `expanduser`,
`size`, `linecount`, `hash`, and `digest`; the tests `dir`, `file`, `symlink`,
`pipe`, `block_device`, `char_device`, and `device`.
- Clock-injected: `now()` is the real helper only when the surface's
`StdlibConfig` carries a clock supplied through `with_clock`, and a refusal
when it would read the ambient clock.

Each refusal is a MiniJinja `InvalidOperation` error whose message is the
Fluent key `stdlib.test_mode.helper_unavailable` with argument `operation`,
prefixed by the stable code `[netsuke::jinja::test_mode::unavailable]` in every
catalogue, and whose source is a typed `TestModeRefusal { operation }`. Stubs
accept any positional and keyword arguments, because a real helper documents
several call shapes and a narrower signature would report an argument-count
error instead of the refusal it must deliver.

Two rows are deliberate deviations from a "run it against an empty sandbox"
reading, and both were reviewed before implementation:

- `which` refuses although `path_override` and `pathext_override` exist.
Running it against an empty `PATH` reports a misleading "not found" rather
than a refusal naming the withheld capability, and no sandbox-rooted resolver
exists yet.
- `expanduser` refuses rather than resolving `~`. With
`HomeDirectory::Missing` the real filter would fail anyway, and a resolved
home path exposes a host-shaped answer.

Template overlays grant the test author a function name in the MiniJinja
environment, backed by a caller-supplied closure. Registration happens after
the stdlib, the manifest `vars`, and the manifest macros, and before `foreach`
expansion, so an overlay is visible to `when`, `foreach`, and every rendered
field and can replace a same-named helper. Overlay names are validated against
the namespaces the loader and manifest already claim — `env`, the `__netsuke_`
prefix, a `vars` key, and a manifest macro — and a collision is a typed
`TemplateOverlayError::NameCollision` rendered through
`manifest.overlay.name_collision` with code
`[netsuke::manifest::overlay::collision]`. Validation runs before any
registration, so a rejected overlay leaves the environment untouched.

MiniJinja offers no lookup by name, but registered functions are enumerable as
environment globals. Before replacing a name the loader captures the displaced
function and hands it to the overlay as `OverlayCall::shadowed`, so an overlay
can wrap a helper rather than only discard it. This is the capture a spy
depends on.

Of the refused rows, only the functions — `glob`, `which`, `fetch`,
`command_available`, and a system-clocked `now` — are replaceable by overlays
in the first version. Filters and tests are not mockable (UX design §8.6);
roadmap 7.4.1 adds sandbox-rooted adapters for `glob`, the file tests, and the
file-reading filters.

## Rationale

A fourth enum variant keeps one decision table for load capabilities. Adding a
separate `TestMode` flag, or a parallel registration path, would let two
mechanisms disagree about, for example, whether `glob` reaches the filesystem,
and a caller could combine them into a load neither designed.

The inventory-driven surface makes the deny-by-default rule mechanical. A
missed helper is a contract-test failure rather than a silent capability grant,
and the two dispositions sit side by side so query and test behaviour cannot
drift apart.

Refusing rather than omitting is the difference between a manifest author
seeing "unknown function `fetch`" (which suggests a typo) and seeing that
`fetch` is unavailable under `netsuke test` and that an overlay is the way to
answer it. The typed source keeps the distinction available to code, not just
to a human reading the message.

Composing with the existing `StdlibConfig` and `disabled_env_reader` avoids
inventing a test-only stdlib. The surface narrows what the build path already
provides and reuses its diagnostic and environment shapes.

## Consequences

- A manifest that calls a withheld helper fails under test with a located,
localized diagnostic naming the operation; it never receives a host-dependent
answer.
- A test can answer `glob`, `which`, `fetch`, `command_available`, and a
system-clocked `now` with an overlay, and can wrap the displaced function
rather than replacing it.
- Filters and tests cannot be mocked in the first version; a manifest that
pipes through `shell` or uses `file` remains untestable until roadmap 7.4.1.
- `StdlibRegistration` stays crate-private, so the loader's four modes are not
an embedder-facing contract.
- Query loads keep their existing behaviour: the same inventory answers them,
and their refusal text is unchanged.
- Test and query loads record no expansion or budget telemetry
([ADR-0050](adr-0050-bound-manifest-template-evaluation.md) Addendum E),
since those counters describe normal manifest loading.

## Alternatives considered

- A boolean `test_mode` flag on the load options. Rejected: it splits the
capability decision across a flag and the existing registration, and admits
combinations neither mode defines.
- Registering nothing for withheld helpers. Rejected: MiniJinja's unknown-name
error does not say the capability was withheld, and no typed source is
available for callers.
- Building a second, test-only stdlib. Rejected: it duplicates the surface,
drifts from build behaviour, and would need its own diagnostics.
- Intercepting MiniJinja's unknown-function error centrally. Rejected: the
refusal would lose the operation name and the call-shape reach the stubs
preserve.
- Granting the real helpers against a sandbox root. Rejected for 7.1.2: `glob`,
the file tests, and the file-reading filters do not route through
`StdlibConfig::workspace_root` yet; that work is roadmap 7.4.1.

## Implementation references

- `src/manifest/loader_options.rs` — `StdlibRegistration` and
`ManifestLoadOptions`.
- `src/manifest/test_surface/mod.rs` — `TestSurface`.
- `src/manifest/test_surface/overlays.rs` — `TemplateOverlays`,
`OverlayCallable`, `OverlayCall`, name validation, and registration order.
- `src/stdlib/restricted_helpers/mod.rs` — the `SURFACE` inventory.
- `src/stdlib/restricted_helpers/test_mode.rs` — `TestModeRefusal` and the
refusing registrations.
- `src/stdlib/restricted_helpers/query.rs` — the query disposition and its
refusal diagnostics.
- `src/manifest/env/reader.rs` — `disabled_env_reader`.

## References

- [Technical design §4.2, §4.3, §5.5, and §7](../netsuke-test-framework-technical-design.md)
- [UX and semantic design §8.6](../netsuke-test-framework-ux-design.md)
- [ADR-0008: environment seam taxonomy](adr-0008-environment-seam-taxonomy.md)
- [ADR-0010: glob capability scoping](adr-0010-scope-the-glob-metadata-capability-to-the-literal-prefix.md)
- [ADR-0050: bound manifest template evaluation](adr-0050-bound-manifest-template-evaluation.md)
- [RFC 0007: Netsukefile testing framework](../rfcs/rfc-0007-netsukefile-testing-framework.md)
- [Roadmap items 7.1.2 and 7.4.1](../roadmap.md#71-seams-and-loader-options)
3 changes: 3 additions & 0 deletions docs/contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ operator, user, and contributor references are easier to find.
- [ADR-0069](adrs/adr-0069-split-the-localization-key-tables.md): Splitting the
Fluent key tables by audience under Whitaker's module line cap, while the
build audit and `keys::ALL_KEYS` keep seeing one key set.
- [ADR-0070](adrs/adr-0070-deny-by-default-test-mode-stdlib.md):
Deny-by-default stdlib surface for `netsuke test`, typed refusal stubs, and
template overlays that replace withheld functions by name.

## Proposals

Expand Down
Loading
Loading