Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
9a8ac22
Draft the execplan for the stdlib clock provider seam (7.1.1)
leynos Sep 8, 2026
95fd0c2
Apply canonical Markdown formatting to the 7.1.1 execplan
leynos Sep 8, 2026
49a6e26
Satisfy the spelling and Markdown gates in the 7.1.1 execplan
leynos Sep 8, 2026
b320680
Revise the 7.1.1 execplan after the design review
leynos Sep 8, 2026
1020008
Add failing tests for the stdlib clock seam (7.1.1, EP-M0)
leynos Sep 10, 2026
5a3bc5d
Thread the stdlib clock provider seam through now() (7.1.1, EP-M1)
leynos Sep 10, 2026
fd1202e
Add integration and BDD coverage for the stdlib clock seam
leynos Sep 10, 2026
d484643
Split the time test module and unshadow the BDD clock step
leynos Sep 10, 2026
1b5ffee
Record EP-M3 gate results in the 7.1.1 exec plan
leynos Sep 10, 2026
c0f3d47
Record the six mutation outcomes in the 7.1.1 exec plan
leynos Sep 10, 2026
dcdd273
Classify the stdlib clock seam in the documentation (7.1.1, EP-M4)
leynos Sep 10, 2026
9d85390
Close roadmap 7.1.1 and complete the exec plan (EP-M5)
leynos Sep 10, 2026
d6ce762
Record the implemented clock seam in RFC 0007 and fix the spelling gate
leynos Sep 10, 2026
b81cbcc
Record the final gate and CodeRabbit evidence in the 7.1.1 exec plan
leynos Sep 10, 2026
b265413
Record the pull request description replacement in the 7.1.1 exec plan
leynos Sep 10, 2026
ea6c4db
Validate the 7.1.1 branch tip and close its evidence chain
leynos Sep 10, 2026
71a7b71
Extract configure_stdlib from render_template_with_context
leynos Sep 10, 2026
559a80c
Record the rebase and the configure_stdlib extraction in the 7.1.1 ex…
leynos Sep 10, 2026
50b5671
Apply canonical Markdown formatting to the 7.1.1 exec plan
leynos Sep 10, 2026
5ee77b4
Record the ready-for-review flip in the 7.1.1 exec plan
leynos Sep 11, 2026
46dd557
Record the withdrawn CodeRabbit finding in the 7.1.1 exec plan
leynos Sep 11, 2026
f4ffa99
Record the scope-tolerance exception in the 7.1.1 exec plan
Sep 19, 2026
6edd2b5
Sharpen the non-plan remainder figure in D14
Sep 19, 2026
0ff685c
Extract the clock seam from config/mod.rs into a sibling module
Sep 19, 2026
ca3d6e9
Re-anchor D14's figures to the rebased branch
Sep 19, 2026
28683dc
Apply end-of-line table reflow to the plan's D14 table
Sep 19, 2026
283b8e6
Re-quote D14's totals at the head that carries the record
Sep 19, 2026
84e36ec
Qualify the plan's note on unreachable commit identifiers
Sep 19, 2026
e9b0c05
Rewrap the plan to mdtablefix's CI flag set
Sep 19, 2026
69385f5
Record the mdtablefix flag-set mistake as artefact entry 18
Sep 19, 2026
5180475
Bring the plan's Progress checklist up to the current head
Sep 19, 2026
a8b2137
Document the clock seam in the users' guide and migration guide
Sep 19, 2026
b303c1a
Flip the documentation bullet to done in the plan's Progress checklist
Sep 19, 2026
2c2a16b
Re-anchor D14's figures to the head that carries the record
Sep 19, 2026
f0e984a
Add the row for the head that carries the record to D14's table
Sep 19, 2026
009004e
Quote only durable figures in the scope-escalation retrospective
Sep 19, 2026
b140beb
Record the verified CI state of the final head in the Progress checklist
Sep 19, 2026
96c88dc
Dispose of the second review round's verified findings
Sep 19, 2026
eadbf9f
Record why the Windows gate fails, and that it is not this branch's
Sep 19, 2026
7b30955
Record the pending review request against the head it targets
Sep 19, 2026
c5fb347
Re-target the branch onto the current origin/main tip
Sep 19, 2026
9e9098f
Dispose of the review findings on the clock seam
Sep 19, 2026
3137e13
Apply rustfmt to the review-disposition commit
Sep 19, 2026
bfb6165
Record the completed review and both findings' dispositions in the plan
Sep 19, 2026
341aba6
Propagate the registration failure instead of asserting in a Result test
Sep 19, 2026
62708c6
Record why the local make test timeout is not this branch's defect
Sep 19, 2026
a29e066
Record the tracker for the local make test timeout
Sep 19, 2026
f70e0fd
Record the published re-target and the reply heads
Sep 19, 2026
4e4f78c
Record the CI verdict for the published head
Sep 19, 2026
e3953a7
Record the reconciled review surfaces and the open decision
Sep 19, 2026
6895d89
Name the verified head and stop re-verifying it
Sep 19, 2026
0de36a0
Correct the reason the Observability row still renders
Sep 19, 2026
cabb7c6
Record the re-rebase and correct the superseded timeout disposition
Sep 24, 2026
c4e2c29
Record publication and the disposition of the two declined checks
Sep 24, 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
34 changes: 34 additions & 0 deletions docs/adr-008-environment-seam-taxonomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ resolution entirely rather than setting the variable for a child to read.
going through `NETSUKE_NINJA` resolution at all
- `EnvReader`: [`src/manifest/env_reader.rs`](../src/manifest/env_reader.rs)
(manifest `env()` Jinja helper)
- Clock seam: [`src/stdlib/time/clock.rs`](../src/stdlib/time/clock.rs)
(`ClockProvider`, `system_clock`, `fixed_clock`); `StdlibConfig::with_clock`
in [`src/stdlib/config/clock.rs`](../src/stdlib/config/clock.rs) is the
injection point, and [`src/stdlib/register.rs`](../src/stdlib/register.rs)
captures the provider when it registers `now()`
- Child-environment composition:
[`test_support/src/netsuke.rs`](../test_support/src/netsuke.rs)
(`run_netsuke_in_with_env`) and `tests/bdd/steps/manifest_command_helpers.rs`
Expand Down Expand Up @@ -234,3 +239,32 @@ process-global environment or working-directory changes. Route B avoids CWD
changes by passing absolute paths or preserving `-C/--directory` for automatic
project discovery. Explicit relative `--config` and `NETSUKE_CONFIG` selectors
remain anchored to the child process CWD; they are not rebased beneath `-C`.

### 2026-09-11: Stdlib clock seam

The stdlib `now()` helper reads its instant through an injected
`ClockProvider`, an `Arc<dyn Fn() -> OffsetDateTime + Send + Sync>` held by
`StdlibConfig` and captured by the registered Jinja function. It takes the
`EnvReader` shape, not a narrow closure and not `mockable::Env`, for the same
reason `EnvReader` does: `minijinja` requires registered functions to be
`Send + Sync`, so a borrowed closure parameter cannot satisfy the bound.
`StdlibConfig` is the clock's single owner; `system_clock()` is the sole
production supplier and the only place `OffsetDateTime::now_utc` is called for
`now()`. Manifest-query registration receives no clock and keeps its refusing
`now` stub.

The taxonomy is applied here to an ambient input that is *not* an environment
variable. This ADR's context section is written about `clippy.toml`'s ban on
`std::env::var` and friends, and no lint forbids reading the clock; the shape
rubric transfers, the original scope does not. Note also that `StdlibConfig`
now holds two ambient seams in two shapes — `home_directory: HomeDirectory`, a
resolved value, and the clock, a closure. The clock's shape is the one this
rubric prescribes for a `Send + Sync` registration point; `HomeDirectory` is
the outlier, and the two should not be "harmonized" without revisiting this
entry. `ClockProvider` also puts `time::OffsetDateTime` on netsuke's public
surface, so a `time` 0.4 bump is a breaking library-API change.

`mockable::Clock` was not used: it is typed in `chrono`, which this workspace
does not depend on, so adopting it would add a second date-time crate to render
one timestamp. `monotony`, already a dependency, abstracts only monotonic
elapsed time and has no wall-clock type.
13 changes: 13 additions & 0 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -5025,6 +5025,19 @@ pure collection filters without environment state. Keep these registration
functions as feature-local wiring points rather than calling them independently
from manifest code.

The stdlib's `now()` helper reads through a `ClockProvider`
(`src/stdlib/time/clock.rs`), an `Arc`-wrapped `Fn() -> OffsetDateTime` in the
`EnvReader` shape and for the same reason: registration requires `Send + Sync`.
`StdlibConfig` is the clock's single owner — `with_clock` replaces the provider,
`system_clock()` is the production adapter and the only place the helper reads
the host clock, and `fixed_clock` supplies a deterministic instant to tests.
Keep the seam confined to the `stdlib::time` registration path: manifest-query
registration installs the clock-independent helpers only and keeps refusing
`now`, and the provider is not a general time service for the crate. The clock
is not an environment variable and no lint polices it, so
[ADR-008](adr-008-environment-seam-taxonomy.md) supplies the shape rubric here
but not its original scope.

`CommandConfigInit` is the internal hand-off from `StdlibConfig` to command
helpers. It carries the capability-scoped workspace root, output limits, and an
optional `PATH` override. `CommandConfig::new` consumes the owned bundle, and
Expand Down
2,972 changes: 2,972 additions & 0 deletions docs/execplans/7-1-1-clock-provider-seam.md

Large diffs are not rendered by default.

32 changes: 22 additions & 10 deletions docs/netsuke-test-framework-technical-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,22 +259,34 @@ builds one from the case's `given.env` map: declared names return their values,
environment is reachable only through an explicit future opt-in; the default
reader never consults it (C2, C4).

### 5.2. Clock (new seam)
### 5.2. Clock

`now()` currently calls `OffsetDateTime::now_utc()` directly
(`src/stdlib/time/mod.rs:62`) — a gap relative to ADR-008. The stdlib time
module gains a clock provider in the `EnvReader` shape (an `Arc` closure,
because MiniJinja registration requires `Send + Sync`):
Implemented. The stdlib time module owns a clock provider in the `EnvReader`
shape (an `Arc` closure, because MiniJinja registration requires `Send + Sync`):

```rust
pub type ClockProvider = Arc<dyn Fn() -> OffsetDateTime + Send + Sync>;
```

Production registration wraps `OffsetDateTime::now_utc`; the test runner
supplies a fixed instant parsed from `given.clock.now`. The seam lives in
`StdlibConfig` alongside the existing `path_override` and `home_directory`
knobs — the clock's single owner — and is a prerequisite refactor deliverable
in its own right.
It lives in `src/stdlib/time/clock.rs` with `system_clock()`, the production
adapter wrapping `OffsetDateTime::now_utc`, and `fixed_clock(instant)`, the
deterministic adapter. The seam is held in `StdlibConfig` alongside the existing
`path_override` and `home_directory` knobs — the clock's single owner — and
`with_clock` is the injection point; `register_functions` captures the provider
when it installs `now()`, so each evaluation reads the provider again, and no
provider-less call path can bypass it.

`StdlibConfig` stores the provider in a private `WallClock` container, a
mechanical addition this design did not name. It confines a handwritten `Debug`
(a closure is not printable, so `StdlibConfig` could not have derived one) and
normalizes every read to UTC, because an injected provider is free to return
any offset while `now()` is documented to yield UTC. Manifest-query
registration receives no clock and keeps its refusing `now` stub.

The test runner's `given.clock.now` input
([UX design §7](netsuke-test-framework-ux-design.md)) is not yet wired to this
seam; the BDD suite drives it with an explicit clock fixture step instead. That
wiring belongs to the runner slices, for which this seam is the prerequisite.

### 5.3. Network (policy, not transport)

Expand Down
60 changes: 30 additions & 30 deletions docs/rfcs/0006-ansible-inspired-template-standard-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,26 +150,29 @@ Three existing mechanisms matter to this proposal.

### 3.3. Known weaknesses in the current surface

Three existing gaps constrain this design and are called out so the follow-up
work does not silently inherit them.
Three gaps constrained this design at the time of writing and are called out so
the follow-up work does not silently inherit them. The first two remain open;
the third has since been closed.

- **Excluded helpers do not all fail explicitly.** `register_manifest_query`
stubs six helpers: `env`, `glob`, `fetch`, `shell`, `grep`, and `contents`. A
further sixteen names registered in the full environment are absent from the
manifest-query environment altogether, so a manifest query reports "unknown
filter" or "unknown test" rather than explaining the restriction. They are
the filters `realpath`, `expanduser`, `size`, `linecount`, `hash`, and
`digest`; `which`, which is registered as both a filter and a function; the
functions `command_available` and `now`; and the file tests `dir`, `file`,
`symlink`, `pipe`, `block_device`, `char_device`, and `device`. Section 6.2
makes explicit failure normative, and section 14.1 schedules the repair
across that whole set rather than the path filters alone.
stubs fifteen helpers: `env`, `glob`, `fetch`, `shell`, `grep`, and
`contents`, then `realpath`, `expanduser`, `size`, `linecount`, `hash`,
`digest`, `which`, `command_available`, and `now`. Each raises a restriction
diagnostic rather than "unknown filter" or "unknown test". A further seven
names registered in the full environment are absent from the manifest-query
environment altogether: the file tests `dir`, `file`, `symlink`, `pipe`,
`block_device`, `char_device`, and `device`. Section 6.2 makes explicit
failure normative, and section 14.1 schedules the repair of that remaining
set.
- **`manifest_query_operation_error` is not localized.** It builds its message
with `format!` rather than a Fluent key, unlike the rest of the stdlib.
- **`now` has no injected clock seam.** It calls `OffsetDateTime::now_utc()`
directly. The time helpers proposed here are pure and do not need the seam,
but the gap is recorded because it bounds how far time behaviour can be
tested deterministically.
- **`now` had no injected clock seam.** It called `OffsetDateTime::now_utc()`
directly. The time helpers proposed here are pure and did not need the seam,
but the gap was recorded because it bounded how far time behaviour could be
tested deterministically. Roadmap item 7.1.1 has since closed it: `now()`
reads through a `ClockProvider` held by `StdlibConfig`, classified in the
[ADR-008](../adr-008-environment-seam-taxonomy.md) addendum for 2026-09-11
and answered as question 7 in section 16.

## 4. Goals and non-goals

Expand Down Expand Up @@ -1917,17 +1920,13 @@ each invent their own version of the same shared machinery.
once clause 2 of section 6.2 is satisfied.
- The repair of the two existing gaps recorded in section 3.3. This slice
localizes `manifest_query_operation_error` through a Fluent key, and it adds
an explicit stub for every one of the sixteen names that section 3.3 records
as absent from the manifest-query environment, so no helper silently
an explicit stub for each of the seven names that section 3.3 records as
still absent from the manifest-query environment, so no helper silently
disappears from a manifest query. Every stub raises the same localized
manifest-query restriction diagnostic. The names are:
- the filters `realpath`, `expanduser`, `size`, `linecount`, `hash`, and
`digest`;
- `which`, which needs a stub in both its filter form and its function form,
because filters and functions occupy separate namespaces;
- the functions `command_available` and `now`; and
- the tests `dir`, `file`, `symlink`, `pipe`, `block_device`, `char_device`,
and `device`.
manifest-query restriction diagnostic. The remaining names are the tests
`dir`, `file`, `symlink`, `pipe`, `block_device`, `char_device`, and
`device`; the other nine of the original sixteen are already stubbed (section
3.3).
- The **maintained inventory** in
[the standard-library guide](../stdlib-yaml-and-jinja-guide.md): one table
distinguishing MiniJinja built-ins, existing Netsuke extensions, adopted
Expand Down Expand Up @@ -2098,10 +2097,11 @@ Windows host needs when generating paths for a Unix target.
6. **Should `text_hash` gain a truncating sibling?** The existing `digest`
filter is `hash` plus a length. If `text_hash` proves useful, `text_digest`
is the obvious follow-on. It is not proposed here for want of a use case.
7. **Does `now` need an injected clock seam?** Section 3.3 records the gap.
Nothing in this RFC requires it, because `to_datetime` and `strftime` are
pure, but a future slice that wants deterministic time tests will have to
answer it.
7. **Does `now` need an injected clock seam?** Resolved. Section 3.3 records
the gap, and nothing in this RFC required the seam, because `to_datetime` and
`strftime` are pure. Roadmap item 7.1.1 supplied it: `now()` reads through a
`ClockProvider` held by `StdlibConfig`, classified in the
[ADR-008](../adr-008-environment-seam-taxonomy.md) addendum for 2026-09-11.

## 17. Recommendation

Expand Down
17 changes: 10 additions & 7 deletions docs/rfcs/0007-netsukefile-testing-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ of capability-scoped non-build loading. The test runner is a third mode of that
same shape, so it extends the established pattern instead of introducing a
parallel one; the technical design records the consequences.

What is missing: a clock seam for `now()` (it calls the system clock directly),
a mechanism to substitute manifest macros, any test dialect, discovery, mock
engine, fixture lifecycle, or `test` subcommand. The manifest schema rejects
unknown top-level keys, so the proposed `tests` configuration block is a schema
What is missing: a mechanism to substitute manifest macros, any test dialect,
discovery, mock engine, fixture lifecycle, or `test` subcommand. (The clock
seam for `now()` that this section originally listed was supplied by roadmap
item 7.1.1; the [technical design](netsuke-test-framework-technical-design.md)
§5.2 records the implemented shape.) The manifest schema rejects unknown
top-level keys, so the proposed `tests` configuration block is a schema
addition with compatibility consequences (see below).

## Goals and non-goals
Expand Down Expand Up @@ -101,9 +103,10 @@ helpers, and carries results over length-prefixed `serde_json` frames versioned
like the existing JSON envelope, so it adds no new dependency. The same stream
carries incremental journal checkpoints, so a case killed on the deadline still
reports the calls it had already made rather than an empty journal. Two seams
are added (clock provider; macro substitution overlay); network mocking needs
no transport seam because the deny-all policy plus function-level doubles make
the real network code unreachable under test.
are added (the clock provider, supplied by roadmap item 7.1.1, and the macro
substitution overlay); network mocking needs no transport seam because the
deny-all policy plus function-level doubles make the real network code
unreachable under test.

Positioning within the product: phase 3 of the roadmap makes Netsuke
predictable for humans and automation; phase 4 verifies the compiler itself;
Expand Down
10 changes: 5 additions & 5 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -1341,15 +1341,15 @@ Objective: deliver the `netsuke test` command and YAML test dialect specified in

### 7.1. Seams and loader options

- [ ] 7.1.1. Add the clock provider seam to the stdlib time module. See
- [x] 7.1.1. Add the clock provider seam to the stdlib time module. See
[technical design §5.2](netsuke-test-framework-technical-design.md).
- [ ] Register `now()` through an injected `ClockProvider` closure held in
- [x] Register `now()` through an injected `ClockProvider` closure held in
`StdlibConfig`.
- [ ] Preserve current behaviour when no provider is supplied.
- [ ] Test an injected provider value, repeated `now()` calls returning
- [x] Preserve current behaviour when no provider is supplied.
- [x] Test an injected provider value, repeated `now()` calls returning
it, and the ambient fallback when no provider is configured, all
registered through `StdlibConfig`.
- [ ] Record the seam classification per
- [x] Record the seam classification per
[ADR-008](adr-008-environment-seam-taxonomy.md).

- [ ] 7.1.2. Introduce the options-carrying manifest loader entry point. See
Expand Down
45 changes: 45 additions & 0 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,51 @@ are ignored, matching the existing accessible reporter contract; applications
can observe them through the bounded timing sink telemetry emitted by their
configured metrics and tracing backends.

### Inject the clock for deterministic tests

`now()` does not read the host clock directly. It reads through an injectable
`ClockProvider` seam held by `StdlibConfig`, so tests and other callers can pin
the instant instead of racing a real clock. The default remains the ambient
host clock, so existing templates and manifests are unaffected.

- `StdlibConfig::with_clock` accepts a `ClockProvider`, replacing the wall-clock
source that `now()` reads.
- `fixed_clock(instant)` builds a provider that always reports `instant`.
- `system_clock()` builds the host-backed provider that the default
configuration uses.
- `ClockInstant` re-exports the provider's timestamp type, so a caller can name
that type without adding its own `time` dependency.

Registration captures the adapter that holds the provider, and each `now()`
call invokes it to read the instant afresh, so a provider that yields a
different instant on each call is observed by successive `now()` evaluations.
Readings are normalized to UTC, and an explicit `offset=` argument re-expresses
the same instant in the requested offset rather than changing it.

Manifest-query registration still refuses `now()`, so the seam does not widen
what a manifest query may evaluate.

<!-- tested-example: guide-clock-snippet -->

```rust
use minijinja::Environment;
use netsuke::stdlib::{self, StdlibConfig, fixed_clock};
use time::macros::datetime;

let instant = datetime!(2026-06-08 12:00:00 UTC);
let config = StdlibConfig::from_current_dir()
.expect("open workspace")
.with_clock(fixed_clock(instant));

let mut env = Environment::new();
stdlib::register_with_config(&mut env, config).expect("register stdlib");
let rendered = env.render_str("{{ now() }}", ()).expect("render");
assert_eq!(rendered, "2026-06-08T12:00:00Z");
```

This snippet mirrors the executable doctest on `with_clock` in the API
documentation, rather than the YAML-only examples elsewhere in this guide.

### Use the canonical build graph

`BuildGraph` stores each logical build edge once. Every output alias, explicit
Expand Down
Loading
Loading