Skip to content

Plan: Add the clock provider seam to the stdlib time module (7.1.1) - #696

Draft
leynos wants to merge 4 commits into
mainfrom
7-1-1-clock-provider-seam
Draft

Plan: Add the clock provider seam to the stdlib time module (7.1.1)#696
leynos wants to merge 4 commits into
mainfrom
7-1-1-clock-provider-seam

Conversation

@leynos

@leynos leynos commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Draft ExecPlan for roadmap item 7.1.1, which makes the Netsuke stdlib now()
Jinja function testable by reading its instant through an injected clock
provider instead of calling OffsetDateTime::now_utc() directly.

This PR contains the plan only — no implementation. The plan must be
approved before any code is written.

Plan:
docs/execplans/7-1-1-clock-provider-seam.md

What the plan delivers

A caller that builds a StdlibConfig may supply a ClockProvider, and every
now() call in that Jinja environment returns exactly that instant. A caller
that supplies nothing keeps today's behaviour precisely. There is no
user-visible change: a manifest author sees identical now() behaviour before
and after.

The seam is a prerequisite refactor for the Netsukefile testing framework
(roadmap phase 7), specified by
technical design §5.2,
and is deliberately scoped as a deliverable in its own right — no netsuke test command, no test dialect, no src/testing module.

Design decisions worth a reviewer's attention

  • Port shape. ClockProvider = Arc<dyn Fn() -> OffsetDateTime + Send + Sync>,
    the EnvReader shape. ADR-008 justifies that shape by MiniJinja's Send + Sync requirement, but that argument is necessary and not sufficient — a
    Box satisfies it too. The decisive constraint is that StdlibConfig
    derives Clone, which Box<dyn Fn> cannot provide (D1).
  • The strongest alternative was a resolved-value enum in the
    HomeDirectory shape (Ambient / Fixed(OffsetDateTime)), which would
    derive Debug and Clone for free. It is rejected in D10 — though note the
    first draft rejected it for the wrong reason, claiming the enum made the
    per-call negative control unwriteable. It does not; an enum could carry a
    Sequence variant. The surviving argument is cohesion: preserving that
    control under an enum means adding a test-only variant to a production type,
    forcing every match site to service a case production never takes.
  • A WallClock newtype absorbs the Debug problem. StdlibConfig derives
    Debug and Arc<dyn Fn> does not implement it; a one-field newtype with a
    handwritten impl confines the boilerplate, following the existing
    CommandEnv precedent. It is named WallClock rather than Clock because
    Clock already names a monotonic clock generic in
    src/runner/process/mod.rs, alongside two other MonotonicClock spellings.
  • Two crates that look like ready-made answers are dead ends.
    mockable::Clock is chrono-typed and sits behind a feature this workspace
    does not enable; monotony abstracts monotonic elapsed time only and has no
    wall-clock type. Both were checked against published API docs (D3).
  • ADR-008's jurisdiction is being extended. Its context section is scoped
    to environment variables, and no lint forbids reading the clock. D11
    records that applying the taxonomy to a clock is a decision, not an
    inheritance, and the addendum must say so.

Verification approach

Seven obligations, each paired with the concrete mutation it must reject. The
plan requires a one-off mutation exercise before sign-off, of which the most
important is: store the clock on StdlibConfig but never pass it to
time::register_functions
. That mutation compiles cleanly and passes every
unit test, failing only the integration and behavioural layers — which is why
those layers are mandatory rather than optional.

A six-lens design review corrected two errors of substance before this PR was
opened. Both are called out because both would have shipped as false assurance:

  • The query-mode obligation was vacuous as first written. The refusing
    now stub is registered after the permissive query helpers, and
    MiniJinja's add_function is last-write-wins — so a clock leaked into
    register_query_functions would be silently masked by the stub, and a test
    asserting only "query mode refuses now" would still pass. The obligation
    now carries a second test asserting now is undefined after the permissive
    half alone.
  • Nothing pinned the offset of the injected path. system_clock() yields
    UTC, but an arbitrary provider need not; a fixture built from a local-time
    literal would render a non-Z timestamp, making the harness assert behaviour
    production never exhibits. WallClock::read now normalizes to UTC, with a
    non-UTC-provider case guarding it.

Two pre-existing gaps the planning surfaced, both now closed by the plan:

  • No test anywhere in the repository asserts that now() is refused in
    manifest-query mode, despite docs/users-guide.md promising it.
  • RFC 0006 §3.3 recorded this exact gap and left it as open question 7, which
    no roadmap item cross-referenced. Answering it is now a plan deliverable.

Property testing covers the offset invariant (the same instant, re-expressed);
Kani and Verus are explicitly ruled out with reasons, since the only arithmetic
involved belongs to the time crate and is treated as an axiom.

Roadmap

Item 7.1.1 is marked done by the implementor on completion, not by this PR.

References

🤖 Generated with Claude Code

Summary by Sourcery

Approve an execution plan for introducing a testable clock seam to the stdlib time helpers without implementing the seam itself.

Enhancements:

  • Add a draft execution plan for making the stdlib now() helper deterministic through an injected clock provider while preserving ambient-clock and manifest-query behaviour.

Documentation:

  • Document the proposed clock seam, design decisions, verification obligations, implementation milestones, and required documentation updates for roadmap item 7.1.1.

@coderabbitai

coderabbitai Bot commented Sep 8, 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.

codescene-access[bot]

This comment was marked as outdated.

@sourcery-ai

sourcery-ai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This documentation-only PR adds a draft ExecPlan for making the stdlib now() helper deterministic through an injected, thread-safe clock provider stored in StdlibConfig, with an ambient system-clock fallback, protected manifest-query behavior, explicit design decisions, implementation milestones, and comprehensive verification requirements.

Sequence diagram for configured now() rendering

sequenceDiagram
    participant Caller
    participant Config as StdlibConfig
    participant Registration
    participant Time as time module
    participant Provider as ClockProvider

    Caller->>Config: with_clock(provider)
    Caller->>Registration: register_with_config(config)
    Registration->>Config: clock()
    Registration->>Time: register_functions(WallClock)
    Caller->>Time: Render template containing now()
    Time->>Provider: read provider()
    Provider-->>Time: OffsetDateTime instant
    Time->>Time: to_offset(parsed)
    Time-->>Caller: Rendered timestamp
Loading

File-Level Changes

Change Details Files
Adds a detailed implementation plan for introducing an injectable wall-clock seam while preserving ambient behavior by default.
  • Defines the ClockProvider port, WallClock wrapper, and system/fixed clock adapters.
  • Threads the clock through StdlibConfig and stdlib time registration while keeping query-mode registration clock-free.
  • Preserves UTC normalization, offset handling, Debug/Clone configuration derives, and existing public behavior.
  • Specifies unit, integration, behavioral, property, regression, and mutation testing obligations.
  • Documents the required ADR, technical-design, developer-guide, RFC, and roadmap updates for implementation completion.
docs/execplans/7-1-1-clock-provider-seam.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

leynos and others added 4 commits September 8, 2026 17:11
Plan the injectable `ClockProvider` seam specified in the Netsukefile
testing framework technical design section 5.2, so `now()` can be made
deterministic without changing behaviour for manifest authors.

The plan records the port shape, its ownership by `StdlibConfig`, the
verification obligations with their negative controls, and the seam
classification work ADR-008 and roadmap 7.1.1 require.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run the repository's Markdown formatter over the new plan and split an
over-long trait declaration onto separate lines so the line-length lint
passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Use "handwritten" rather than "hand-written" as the typos gate requires,
and rename the axiom identifiers from AX-n to AXIOM-n so the gate stops
reading the prefix as a misspelling. The longer identifier reflows one
paragraph.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six-lens design review found two errors of substance and three
build-blockers in the prescribed code.

Corrections of substance:

- OBL-5's non-vacuity argument was false. The refusing `now` stub is
  registered after the permissive query helpers, and MiniJinja's
  `add_function` is last-write-wins, so a clock leaked into
  `register_query_functions` would be masked by the stub and the
  obligation would still pass. The obligation now needs two tests, the
  second asserting `now` is undefined after the permissive half alone.
- Nothing pinned the offset of the injected path. An arbitrary provider
  may return a non-UTC instant, which would make the harness assert
  behaviour production never exhibits. `WallClock::read` now normalizes
  to UTC and OBL-1 gains a non-UTC-provider case.

D10's rejection of the resolved-value enum rested on a circular claim
that the enum makes the per-call negative control unwriteable; it does
not. The withdrawn claim is replaced by the cohesion argument, and the
design document's normativity is demoted to a tiebreak because D2 adds a
container the design does not name.

Rename the container to `WallClock`: `Clock` already names a monotonic
clock generic in `src/runner/process/mod.rs` alongside two other
`MonotonicClock` spellings.

Build-blockers fixed: the accessor must be `const fn` without
`#[must_use]`; the sequenced fixture violated the denied
`indexing_slicing` lint and underflowed on an empty vector; and the
`src/stdlib/mod.rs` re-export must land in EP-M1 or its doctests leave
the milestone failing to compile.

Also add `fixed_clock()`, a `ClockInstant` re-export, and an
`is_system()` discriminant so a leaked clock is observable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@leynos
leynos force-pushed the 7-1-1-clock-provider-seam branch from d91ee5f to 1e6c93e Compare September 8, 2026 15:20
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.

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