Skip to content

Align v0.1.0 documentation and test every example - #427

Merged
leynos merged 23 commits into
mainfrom
docs/users-guide-example-coverage
Jul 26, 2026
Merged

leynos merged 23 commits into
mainfrom
docs/users-guide-example-coverage

Conversation

@leynos

@leynos leynos commented Jul 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

This branch gives early adopters an accurate account of Netsuke v0.1.0 and
prevents its user-facing examples from drifting away from executable
behaviour.

It replaces outdated product claims with an honest completion assessment and
road ahead, reorganizes the user's guide around real tasks, corrects linked
example manifests, and adds Rust integration, rstest-bdd, end-to-end, and
property coverage for the README and user's guide examples.

Review follow-ups align the configured default with hello.txt, correct the
photo-edit and writing outputs, and reject unsupported reusable-rule deps
instead of accepting and silently dropping them. Target and action
dependencies remain the supported implicit-dependency contract until the
planned rule-level deps_from feature is implemented.

Review walkthrough

Validation

  • Rebasing: clean replay onto origin/main at 2afbcb6; range-diff found
    one whitespace-only replay artefact, removed in 764e869
  • Documentation-loader properties: valid, duplicate-identifier, and
    unterminated-fence contracts passed; a deliberate duplicate-rejection
    mutation failed and shrank to the minimal input
  • make check-fmt: passed
  • make test: passed, 1,150 tests with no failures or skips
  • make typecheck: passed
  • make lint: passed, including Rustdoc, Clippy, and Whitaker
  • make markdownlint: passed, 69 files with zero errors
  • make nixie: passed
  • git diff --check: passed

Notes

Each manifest example runs in an isolated temporary workspace. The documented
first-run flows execute with real Ninja and assert the exact cat hello.txt
output; the rstest-bdd scenarios retain the novice-flow fake-Ninja process
boundary. Deterministic child-only stubs exercise the photo-edit and writing
manifests without requiring Darktable, Pandoc, or LaTeX.

The CLI rewrite requested in later review feedback was not applied: the live
parser still exposes manifest, build --emit, --diag-json, and the current
presentation flags, and does not expose generate, root --no-input, or
canonical --json. Changing the guide alone would make it inaccurate.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Summary

  • Aligned README, user guide, developer guidance, examples, ADRs and design documents with Netsuke v0.1.0 behaviour.
  • Reworked the CLI around generate, removing the retired manifest command and build --emit; added policy-based output controls, canonical --json, non-interactive validation, and NETSUKE_CONFIG precedence.
  • Added versioned JSON result envelopes for successful build, clean, generate and graph commands, with diagnostics remaining on stderr and help/version passthrough preserved.
  • Rejected unsupported rule-level deps during manifest deserialisation.
  • Added executable documentation coverage, loader/property tests, BDD scenarios, isolated workspaces and Unix Ninja end-to-end tests for README, user-guide and example workflows.
  • Refactored shared test helpers, capability-scoped filesystem access and JSON assertions; fixed stderr-thread cleanup on process-wait failures.
  • Updated the CLI design documentation, ADR-004, Netsuke CLI design, Netsuke design and roadmap to record the implemented contracts.

Validation passed for 1,150 tests, formatting, type checking, linting, Markdown and Nixie validation, and whitespace checks.

Walkthrough

Netsuke’s v0.1.0 workflow now uses generate, typed output policies, unified JSON results and diagnostics, narrowed configuration selection, updated manifest rules, refreshed examples, and executable documentation contract tests.

Changes

v0.1.0 CLI and build workflow

Layer / File(s) Summary
Canonical CLI and configuration contracts
src/cli/*, src/main.rs, src/locale_resolution.rs, src/ast.rs
Replace legacy flags and selectors with typed policies, --json, --no-input, generate, NETSUKE_CONFIG, and explicit rejection of rule-level deps.
Command dispatch and JSON output
src/runner/*, src/json_envelope.rs, src/result_json.rs
Route commands through dispatchers with versioned JSON success documents and JSON-aware stream handling.
Documentation and examples
README.md, docs/*, examples/*, locales/*
Update release documentation, localized help, configuration guidance, and example manifests for the revised CLI and build flows.
Validation coverage
tests/*, .github/workflows/netsukefile-test.yml, build.rs
Align unit, BDD, integration, property, end-to-end, workflow, and documentation tests with the new contracts.

Possibly related issues

  • leynos/actix-v2a#53 — Documentation-example tests cover manifest-time branching and command-selection behaviour related to the issue.

Possibly related PRs

Suggested labels: Roadmap

Suggested reviewers: codescene-access, codescene-delta-analysis

Poem

YAML winds through Jinja bright,
Ninja graphs emerge in flight.
JSON speaks when streams grow still,
Policies shape each build and drill.
Docs and tests now guard the way.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (3 errors, 1 inconclusive)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error src/runner/dispatch.rs adds a new cli.json && output.is_some() generate branch, but tests cover --json generate and generate --output separately, not the combined path. Add an integration/BDD test for netsuke --json generate --output <file> that asserts the file is written and stdout carries the JSON result envelope.
Module-Level Documentation ❌ Error tests/cli_tests/parsing.rs only says 'CLI parsing coverage.', which does not clearly explain the module’s purpose or its link to the parser layer. Expand the module doc in tests/cli_tests/parsing.rs to state that it exercises the Clap-facing CLI parser and merged CLI wiring, not just 'coverage'.
Rust Compiler Lint Integrity ❌ Error build.rs adds module-wide #[expect(dead_code, unused_imports)] on shared source modules, hiding real unused-code diagnostics instead of narrowing the build-script surface. Replace the broad expectations with a smaller build-script module boundary or a dedicated helper that imports only the symbols it actually uses.
Architectural Complexity And Maintainability ❓ Inconclusive placeholder Need to inspect architecture-relevant files before verdict.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the PR’s main scope: aligning v0.1.0 docs and adding example coverage tests.
Description check ✅ Passed The description is clearly related and summarises the documentation, example, and test changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
User-Facing Documentation ✅ Passed docs/users-guide.md documents the new CLI, config, JSON, and safety behaviour; README signposts the core flow; en-US and es-ES locale keys stay aligned.
Developer Documentation ✅ Passed Developer guide, design docs, ADR, roadmap, execplan, and both locales are in sync with the changed CLI/config/runtime boundaries; roadmap items are checked off.
Testing (Unit And Behavioural) ✅ Passed PASS: The PR adds real CLI/E2E tests (assert_cmd, docs, logging) and unit tests cover parser/merge/AST edge cases, failures, and invariants.
Testing (Property / Proof) ✅ Passed PASS: The PR adds proptest/Kani coverage for the new invariants—documented-example parsing, config precedence, Ninja resolution, and cycle/JSON contracts—matching the check.
Testing (Compile-Time / Ui) ✅ Passed PASS: compile-time coverage already uses trybuild in tests/kani_cfg_ui_tests.rs, and the UI/text output changes are guarded by insta snapshots plus focused JSON/string assertions.
Unit Architecture ✅ Passed PASS: command-side effects stay at explicit edges; path/config resolvers remain read-only, and dispatch/result JSON helpers make publication boundaries visible.
Domain Architecture ✅ Passed Rule now models only domain fields, deps is rejected at the parse boundary, and env/fs/CLI concerns stay in manifest/runner adapters.
Observability ✅ Passed PASS: runtime paths add structured tracing at subprocess boundaries, context-rich file I/O errors, and JSON error/result envelopes for startup/runtime failures.
Security And Privacy ✅ Passed Keep it green: the diff adds no secrets or auth changes; JSON/generate paths only emit user-selected artefacts, and new tests use temp workspaces and fake Ninja stubs.
Performance And Resource Use ✅ Passed PASS: The patch only factors duplicate JSON test setup; it adds no unbounded loops, heavy allocations, or hot-path I/O regressions.
Concurrency And State ✅ Passed PASS: stderr forwarding now joins on all exit paths, and a regression test covers the wait() failure path; no unsynchronised shared state was introduced.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/users-guide-example-coverage

Comment @coderabbitai help to get the list of available commands.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review July 23, 2026 16:32

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

chatgpt-codex-connector[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

codescene-access[bot]

This comment was marked as outdated.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]

This comment was marked as resolved.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the docs/users-guide-example-coverage branch from f121d24 to 764e869 Compare July 24, 2026 07:15
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

coderabbitai[bot]

This comment was marked as resolved.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

tests/assert_cmd_tests.rs

Comment on lines +49 to +52

fn generate_streams_to_stdout_by_default() -> Result<()> {
    let temp = setup_simple_workspace("generate stdout test")?;
    let output = create_netsuke_command(temp.path())
        .arg("generate")

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: generate_streams_to_stdout_by_default,generate_streams_to_stdout_with_directory

@leynos

leynos commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

tests/bdd/steps/accessibility_preferences.rs

Comment on file

fn simulated_env(world: &TestWorld) -> impl Fn(&str) -> Option<String> + '_ {
    move |key| match key {
        "NO_COLOR" => world.simulated_no_color.get(),
        "NETSUKE_NO_EMOJI" => world.simulated_no_emoji.get(),

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: prefix_has_non_ascii,prefix_is_ascii

lodyai Bot pushed a commit that referenced this pull request Jul 30, 2026
)

The rebase onto main lands #427 and ADR-004, which remove the legacy
`NETSUKE_CONFIG_PATH` alias and keep `NETSUKE_CONFIG` as the only environment
selector. Reconcile the config-path instrumentation with that decision:

- Drop the `NETSUKE_CONFIG_PATH` branch from `resolve_config_selector`,
  `explicit_config_path`, and their tracing so selection is `--config` then
  `NETSUKE_CONFIG` only.
- Remove the legacy dimension from the discovery tracing tests (fixture,
  scenario struct, and the legacy-only rstest case) and correct the stale
  docstrings that still referenced the legacy variable.
- Track main's `diag_json` to `json` rename in the developers guide while
  keeping the `explicit_config_path -> ConfigPathResolution` signature fix and
  the pure-query/bounded-hash wording.
- Rebuild `Cargo.lock` from main's, adding only the `test_support`
  tracing/tracing-subscriber edges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 30, 2026
Rebase onto main and close the outstanding documentation gaps flagged on the
pull request:

- Add a `tracing_capture` section to the developer guide's test-isolation
  chapter, covering `with_test_subscriber`, the `name=value` field rendering,
  the thread-local subscriber caveat (events from threads spawned inside the
  test are not captured), and the hash-normalization pattern used before insta
  assertions.
- Document the remaining discovery test helpers (`capture_events`, `find_event`,
  `apply_env_setting`, `scenario_cli`, `EventAssertion::new`, the selector case
  test) and the precedence-test helpers (`precedence_winner`,
  `resolve_config_path_with_selectors`, `path_selector`).
- Document `CapturedEventsLayer::on_event` and the `Visit` field renderers.

`Cargo.lock` was taken from main during the rebase and rebuilt afterwards,
re-adding only the `test_support` tracing edges.

The reviewer note asking to restore `NETSUKE_CONFIG_PATH` selector coverage is
not actioned: main removed that legacy alias in #427, and ADR-004 records
`NETSUKE_CONFIG` as the only environment selector, so reintroducing it would
contradict the accepted decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 30, 2026
)

The rebase onto main lands #427 and ADR-004, which remove the legacy
`NETSUKE_CONFIG_PATH` alias and keep `NETSUKE_CONFIG` as the only environment
selector. Reconcile the config-path instrumentation with that decision:

- Drop the `NETSUKE_CONFIG_PATH` branch from `resolve_config_selector`,
  `explicit_config_path`, and their tracing so selection is `--config` then
  `NETSUKE_CONFIG` only.
- Remove the legacy dimension from the discovery tracing tests (fixture,
  scenario struct, and the legacy-only rstest case) and correct the stale
  docstrings that still referenced the legacy variable.
- Track main's `diag_json` to `json` rename in the developers guide while
  keeping the `explicit_config_path -> ConfigPathResolution` signature fix and
  the pure-query/bounded-hash wording.
- Rebuild `Cargo.lock` from main's, adding only the `test_support`
  tracing/tracing-subscriber edges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 30, 2026
Rebase onto main and close the outstanding documentation gaps flagged on the
pull request:

- Add a `tracing_capture` section to the developer guide's test-isolation
  chapter, covering `with_test_subscriber`, the `name=value` field rendering,
  the thread-local subscriber caveat (events from threads spawned inside the
  test are not captured), and the hash-normalization pattern used before insta
  assertions.
- Document the remaining discovery test helpers (`capture_events`, `find_event`,
  `apply_env_setting`, `scenario_cli`, `EventAssertion::new`, the selector case
  test) and the precedence-test helpers (`precedence_winner`,
  `resolve_config_path_with_selectors`, `path_selector`).
- Document `CapturedEventsLayer::on_event` and the `Visit` field renderers.

`Cargo.lock` was taken from main during the rebase and rebuilt afterwards,
re-adding only the `test_support` tracing edges.

The reviewer note asking to restore `NETSUKE_CONFIG_PATH` selector coverage is
not actioned: main removed that legacy alias in #427, and ADR-004 records
`NETSUKE_CONFIG` as the only environment selector, so reintroducing it would
contradict the accepted decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 30, 2026
Record why this branch resolves only `--config` and `NETSUKE_CONFIG`. The
execplan for 3.11.3 still required `NETSUKE_CONFIG_PATH` backward
compatibility, which #427 removed and ADR-004 superseded by making
`NETSUKE_CONFIG` the only environment selector. Add a superseded note to that
plan so the stale requirement is not read as a live constraint; it appears to be
the source of the recurring review request to restore the legacy selector.

Also collapse the duplicated blank lines the rebase auto-merge left in the
developer guide, where main's new "Temporary executable test helpers" section
and this branch's "tracing_capture" section met.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 30, 2026
Address three review findings on the config selector contract.

Fix a real isolation bug in the `clean_config_env` fixture. It returned
`(EnvLock, EnvVarGuard)`, and tuple fields drop in declaration order, so the
lock was released *before* the guard restored `NETSUKE_CONFIG` — the exact race
`EnvLock` exists to prevent, and a violation of the ordering rules the developer
guide already documents. The fixture now acquires the lock first but returns
`(EnvVarGuard, EnvLock)`, so the variable is restored while the lock is held.

Close a tracing coverage gap. `ConfigPathScenario::expected_env_trace` becomes
`Option<(&str, bool)>`: `None` now means "no lookup at all", asserted only for
the CLI short-circuit, while the empty-value and missing-variable cases assert
the `found=false` lookup event that the implementation does emit and that
nothing previously checked.

Make the selector contract explicit rather than implicit. The reviewer asked
where `NETSUKE_CONFIG_PATH` sits in the precedence ladder; it sits nowhere.
ADR-004 (Accepted) records `NETSUKE_CONFIG` as the only environment selector,
#427 removed the legacy alias, and the users guide and design document both
document a two-selector ladder. Taking the finding's stated alternative, the
guide now names the removed alias and cites ADR-004 instead of leaving the
omission ambiguous, and a new `legacy_config_path_variable_is_not_a_selector`
test enforces it: setting the variable alone resolves to `none` and it is never
looked up. Restoring it in `resolve_config_selector` is therefore not actioned,
as that would require superseding an accepted ADR and rewriting three documents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 30, 2026
ADR-004 keeps `NETSUKE_CONFIG` as the only environment selector, and #427
removed the legacy `NETSUKE_CONFIG_PATH` alias. An audit of the normative
documentation found no inconsistencies: the users guide, developer guide, design
document, roadmap, sample configuration, and ADR-004 all already describe the
`--config` > `NETSUKE_CONFIG` > discovery ladder, and the design document's
Mermaid selector diagram has no legacy branch.

The stale requirements live in completed execplans, which a reader can mistake
for live constraints. Add the same short superseded note already carried by
3.11.3 to the four remaining plans that assert legacy support:

- 3.11.2 recorded a decision to keep the alias and the hidden `--config-path`
  surface, later reversed.
- 3.13.2 gave user-facing guidance to select a file with the alias.
- 3.11.1 (both variants) reference the alias, and the derived-config plan's
  manual verification step would now silently select nothing.

The plans' bodies are left intact so the historical record stands.
`netsuke-cli-overhaul.md` needs no note; its references already read
"NETSUKE_CONFIG_PATH -> remove; keep NETSUKE_CONFIG".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 30, 2026
Both plans already carry a top-of-file "superseded in part" note, but two
passages still stated live behaviour in the present tense and would mislead a
reader skimming to them directly.

3.11.2's completion summary listed `NETSUKE_CONFIG_PATH` as the explicit
override at the head of its search order, and cross-referenced a
`netsuke-design.md` section that has since been rewritten. Add a short
historical marker above the list, leaving the list itself intact as the record.

3.11.3 claimed the alias "continues to work as a silent alias for backward
compatibility". Reword to the past tense and note the removal in #427, keeping
the still-accurate point that `NETSUKE_CONFIG` is the documented, user-facing
name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 31, 2026
Document the new instrumentation and the shared test harness:

- Add a `tracing_capture` section to the developer guide's test-isolation
  chapter, covering `with_test_subscriber`, the `name=value` field rendering,
  the thread-local subscriber caveat, and the hash-normalization pattern used
  before insta assertions.
- Extend the existing environment lookup seams section, which already documents
  the `EnvProvider` port, to record that selection stays a pure query and that
  tracing bounds path values to a hash and file name rather than logging full
  paths or formatted parser errors.

Mark the completed execplans whose `NETSUKE_CONFIG_PATH` requirements were
reversed. ADR-004 keeps `NETSUKE_CONFIG` as the only environment selector and
#427 removed the legacy alias, so those plans' present-tense claims are
historical. Their bodies stand as the record; only a superseded note and two
inline markers are added.

Also hyphenate "World-like" in the rstest-bdd migration guide diagram label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 31, 2026
Address four review findings on test and documentation coverage.

Add end-to-end coverage through `merge_with_config`, the boundary `main`
actually calls. The existing tests drive the tracing helpers directly, so
nothing proved the events reach the real pipeline. Both new tests select via
`--config`, which short-circuits the environment lookup, so they assert the
selector, the explicit branch, and the classified load failure without setting
any variable. They also assert the raw path and formatted error stay out of the
events.

Add property tests for the hashing and path helpers, which previously had only
fixed cases. These assert that every correlation hash is 16 lowercase hex
characters for arbitrary bytes, that hashing is deterministic within a run and
never echoes its input, and that path normalization is idempotent and leaves an
absent path unchanged. `DefaultHasher` is not stable across Rust releases, so
nothing asserts a hash value. A table test covers the `.` and `..` forms that
proptest cannot create against a real filesystem.

Add tests for the capture harness's shared state: concurrent snapshots from
cloned handles, recovery from a poisoned lock, and nested subscriber scopes.
These live inside `tracing_capture` because poisoning the lock needs the private
`Arc<Mutex<_>>`, which no public API exposes. The nested-scope test pins the
thread-local stack behaviour the module documents.

Document the discovery module family in the developer guide, covering the
diagnostics, path, assertion-helper, and test modules, and noting why the insta
calls stay in the test modules.

The remaining finding asked to restore `NETSUKE_CONFIG_PATH` tracing. Issue #292
has instead been updated to strike that requirement, since ADR-004 records
`NETSUKE_CONFIG` as the only environment selector and #427 removed the alias.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Aug 1, 2026
Document the new instrumentation and the shared test harness:

- Add a `tracing_capture` section to the developer guide's test-isolation
  chapter, covering `with_test_subscriber`, the `name=value` field rendering,
  the thread-local subscriber caveat, and the hash-normalization pattern used
  before insta assertions.
- Extend the existing environment lookup seams section, which already documents
  the `EnvProvider` port, to record that selection stays a pure query and that
  tracing bounds path values to a hash and file name rather than logging full
  paths or formatted parser errors.

Mark the completed execplans whose `NETSUKE_CONFIG_PATH` requirements were
reversed. ADR-004 keeps `NETSUKE_CONFIG` as the only environment selector and
#427 removed the legacy alias, so those plans' present-tense claims are
historical. Their bodies stand as the record; only a superseded note and two
inline markers are added.

Also hyphenate "World-like" in the rstest-bdd migration guide diagram label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Aug 1, 2026
Address four review findings on test and documentation coverage.

Add end-to-end coverage through `merge_with_config`, the boundary `main`
actually calls. The existing tests drive the tracing helpers directly, so
nothing proved the events reach the real pipeline. Both new tests select via
`--config`, which short-circuits the environment lookup, so they assert the
selector, the explicit branch, and the classified load failure without setting
any variable. They also assert the raw path and formatted error stay out of the
events.

Add property tests for the hashing and path helpers, which previously had only
fixed cases. These assert that every correlation hash is 16 lowercase hex
characters for arbitrary bytes, that hashing is deterministic within a run and
never echoes its input, and that path normalization is idempotent and leaves an
absent path unchanged. `DefaultHasher` is not stable across Rust releases, so
nothing asserts a hash value. A table test covers the `.` and `..` forms that
proptest cannot create against a real filesystem.

Add tests for the capture harness's shared state: concurrent snapshots from
cloned handles, recovery from a poisoned lock, and nested subscriber scopes.
These live inside `tracing_capture` because poisoning the lock needs the private
`Arc<Mutex<_>>`, which no public API exposes. The nested-scope test pins the
thread-local stack behaviour the module documents.

Document the discovery module family in the developer guide, covering the
diagnostics, path, assertion-helper, and test modules, and noting why the insta
calls stay in the test modules.

The remaining finding asked to restore `NETSUKE_CONFIG_PATH` tracing. Issue #292
has instead been updated to strike that requirement, since ADR-004 records
`NETSUKE_CONFIG` as the only environment selector and #427 removed the alias.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
leynos added a commit that referenced this pull request Aug 2, 2026
* Instrument config path resolution (#292)

Add structured tracing around explicit configuration path selection, the
environment lookup, diagnostic layer collection, and explicit load failures, so
users can diagnose why a given file was chosen or why loading failed.

Selection stays a pure query. `resolve_config_selector` returns a
`ConfigPathResolution` recording the winning selector, its path, and every
environment lookup evaluated; the file-layer boundary emits the diagnostics
afterwards. `explicit_config_path_with_env` is retained as a thin wrapper so
existing callers and tests keep their signature.

Path values are bounded to a correlation hash and file name rather than the full
path, and load failures record a `ConfigLoadFailureKind` instead of the
formatted parser error, keeping absolute paths and parser input out of the logs.

Also fix a latent duplication bug surfaced while adding the project-scope
diagnostics. `OrthoConfig` canonicalises every layer path it records, whereas
the expected project path is joined from the caller's `--directory` verbatim, so
a relative or symlinked directory failed the comparison and appended the same
`.netsuke.toml` twice. Because `CliConfig` marks several vectors
`merge_strategy = "append"`, those entries were silently doubled. Both sides now
pass through `normalized_path_key`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Cover config discovery tracing with injected environment (#292)

Add a shared tracing-capture harness to `test_support` and use it to assert the
discovery instrumentation. `with_test_subscriber` installs a capturing layer for
the duration of a closure and renders each event's fields as `name=value`, so a
single implementation serves every module that needs to assert on tracing.

Selection coverage runs through main's injected `TestEnv` double rather than
mutating the process environment, so these tests need neither `EnvLock` nor
`EnvVarGuard`. The table cases assert the selector, the bounded path fields, and
the per-variable lookup trace, including the `found=false` events emitted when
the variable is unset or empty.

`legacy_config_path_variable_is_not_a_selector` pins the closed selector set
recorded by ADR-004: setting `NETSUKE_CONFIG_PATH` alone resolves to `none` and
the variable is never looked up.

`existing_project_scope_layer_is_not_appended_twice` covers the project-scope
duplication fix, passing a directory with a `.` component so the comparison
fails without path normalization.

`EventAssertion` bundles the event and path so the assertion helpers do not
thread repeated `&str`/`&Path` arguments; the insta calls stay in the test
modules so snapshot names remain bound to them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Document config tracing and mark superseded execplans (#292)

Document the new instrumentation and the shared test harness:

- Add a `tracing_capture` section to the developer guide's test-isolation
  chapter, covering `with_test_subscriber`, the `name=value` field rendering,
  the thread-local subscriber caveat, and the hash-normalization pattern used
  before insta assertions.
- Extend the existing environment lookup seams section, which already documents
  the `EnvProvider` port, to record that selection stays a pure query and that
  tracing bounds path values to a hash and file name rather than logging full
  paths or formatted parser errors.

Mark the completed execplans whose `NETSUKE_CONFIG_PATH` requirements were
reversed. ADR-004 keeps `NETSUKE_CONFIG` as the only environment selector and
#427 removed the legacy alias, so those plans' present-tense claims are
historical. Their bodies stand as the record; only a superseded note and two
inline markers are added.

Also hyphenate "World-like" in the rstest-bdd migration guide diagram label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Split discovery diagnostics and path helpers into modules (#292)

The instrumentation pushed `src/cli/discovery.rs` to 457 lines, past Whitaker's
400-line `module_max_lines` cap. Split along concern boundaries rather than
merely relieving the count:

- `discovery_diagnostics.rs` holds the bounded tracing helpers and
  `ConfigLoadFailureKind`, keeping the "never log a full path or a formatted
  parser error" rule in one place.
- `discovery_paths.rs` holds `normalized_path_key`.

Isolating the path helper also narrows a Whitaker exemption. Comparing our
expected project path with the layer paths `ortho_config` records requires
`std::fs::canonicalize`, because that crate canonicalises through `std::fs` and
the comparison has to mirror it; the directory is ambient input that may be
relative or symlinked, which `cap_std` refuses to resolve across directory
boundaries. `excluded_paths` now covers that one 14-line module rather than the
whole of `discovery`.

The layer test creates its fixture directory through `test_support::fs` so the
tests stay under the capability policy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Share tracing capture helpers across discovery tests (#292)

`capture_events` and `find_event` were defined identically in both discovery
test modules. Move them into the shared `event_assertions` module alongside
`EventAssertion`, which both modules already import, and update the module
documentation to describe the wider role.

Behaviour and signatures are unchanged; the duplicated definitions and the
`with_test_subscriber`/`LevelFilter` imports they needed are removed from the
two call sites.

The reviewer's second point, that the developer guide's quality-gate list should
cover `make fmt`, `make markdownlint`, and `make nixie`, is already satisfied:
the guide lists the workspace gates and then those Markdown gates under "For
documentation changes, also run" a few lines below, so no change is needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Cover tracing at the merge boundary and helper invariants (#292)

Address four review findings on test and documentation coverage.

Add end-to-end coverage through `merge_with_config`, the boundary `main`
actually calls. The existing tests drive the tracing helpers directly, so
nothing proved the events reach the real pipeline. Both new tests select via
`--config`, which short-circuits the environment lookup, so they assert the
selector, the explicit branch, and the classified load failure without setting
any variable. They also assert the raw path and formatted error stay out of the
events.

Add property tests for the hashing and path helpers, which previously had only
fixed cases. These assert that every correlation hash is 16 lowercase hex
characters for arbitrary bytes, that hashing is deterministic within a run and
never echoes its input, and that path normalization is idempotent and leaves an
absent path unchanged. `DefaultHasher` is not stable across Rust releases, so
nothing asserts a hash value. A table test covers the `.` and `..` forms that
proptest cannot create against a real filesystem.

Add tests for the capture harness's shared state: concurrent snapshots from
cloned handles, recovery from a poisoned lock, and nested subscriber scopes.
These live inside `tracing_capture` because poisoning the lock needs the private
`Arc<Mutex<_>>`, which no public API exposes. The nested-scope test pins the
thread-local stack behaviour the module documents.

Document the discovery module family in the developer guide, covering the
diagnostics, path, assertion-helper, and test modules, and noting why the insta
calls stay in the test modules.

The remaining finding asked to restore `NETSUKE_CONFIG_PATH` tracing. Issue #292
has instead been updated to strike that requirement, since ADR-004 records
`NETSUKE_CONFIG` as the only environment selector and #427 removed the alias.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Correct load-failure and environment-provider documentation (#292)

Broaden the `ConfigLoadFailureKind` summary. It claimed invalid TOML was the
only `LoadError` case, which is narrower than the behaviour: the variant covers
any failure to load or parse the selected file, including malformed syntax in
any supported format and I/O or permission errors. The variant documentation
already said this and is unchanged.

Fix the environment-provider signature in the developer guide. The section
declared the trait correctly as `EnvProvider`, matching `src/cli/discovery.rs`,
but then showed `resolve_merged_json_with_env` taking `&impl ConfigEnvProvider`,
which does not match `src/cli/diag.rs`. `ConfigEnvProvider` is not a second
trait; it is a public re-export alias declared in `src/cli/mod.rs` so the trait
does not collide with the unrelated `EnvProvider` in `locale_resolution`. The
signature now matches the source, and a sentence records why the alias exists so
the inconsistency is not resolved the wrong way later.

Note the review asked instead for the trait declaration to be renamed to
`ConfigEnvProvider`. That would have made the guide less accurate, since no
trait of that name is declared anywhere.

Finally, note the Markdown gates beside the canonical quality-gate list. They
were already documented, but 62 lines further down, so a reader skimming the
list could miss them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Make the canonical quality-gate list self-contained (#292)

The `## Quality gates` section listed the three workspace commands, then
described the Markdown gates roughly sixty lines further down, past the nextest
and Whitaker prose. Both lists were inside the same section, so the information
was present but not discoverable from the list a reader actually consults.

Promote the Markdown gates into the canonical block as a bulleted list under an
explicit condition, and remove the later duplicate. The three workspace commands
are unchanged.

A prose pointer added earlier was not enough: the commands need to be readable
as a list at the point of use.

The all-six checklist further down is left alone. It belongs to a specific
mutation-testing documentation change and already defers to this section as
authoritative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Restore completed execplans to their implementation-time state (#292)

Revert the annotations added to five completed execplans. A plan marked
COMPLETE is a historical record of the repository at implementation time, so it
should not be edited to reflect later decisions.

The clearest overstep was in 3.11.3, where the plan's own prose was rewritten
from "The existing `NETSUKE_CONFIG_PATH` environment variable continues to work
as a silent alias" into the past tense with an interpolated note about its
later removal. That changes what the document said when it was written. The
inline marker inserted into 3.11.2's search-order section had the same problem,
and the superseded blockquotes on all five plans were unnecessary.

The current contract is already recorded where it belongs: ADR-004 for the
decision, and the developer guide, users guide, and design document for the
behaviour.

* Document the path-normalization fallback policy (#292)

`normalized_path_key` resolves a path through `std::fs::canonicalize` and falls
back to the input when resolution fails. A review read that fallback as silently
discarding an I/O error and hiding an environmental dependency, and asked for a
`Result` or an injected normalizer.

The signature is left alone, because resolution failure is the ordinary case
rather than an error: the usual input is the expected `.netsuke.toml`, which
most often does not exist. Returning `Result` would report that routine absence
as a failure and force every caller to reapply the same fallback to obtain a
comparable key. Comparing literally is sound as well, since an unresolved path
cannot equal a resolved one, so the caller treats the layer as unmatched and
takes the append branch.

What the review is right about is that none of this was written down. Document
the filesystem access and the fallback policy explicitly, explain why the
function is total, and name the tests that pin the behaviour:
`normalized_path_key_is_identity_for_absent_paths` and
`normalized_path_key_is_idempotent`, with `.`/`..` resolution covered by
`normalized_path_key_resolves_non_canonical_forms`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Repair blank-line spacing after the rebase merge (#292)

The rebase onto the Polonius toolchain change auto-merged the developer guide
and left a second blank line before the `tracing_capture` and configuration
discovery module layout headings, which markdownlint rejects under MD012.

* Expose canonicalization failure from the path normalizer (#292)

`normalized_path_key` resolved a path and quietly substituted the input when
canonicalization failed, so an environmental dependency was hidden behind an
infallible signature.

It now takes a `PathNormalizer` and returns `io::Result<PathBuf>`, propagating
the error unchanged. The fallback moves to `collect_file_layers`, which owns the
decision: an unresolvable path is compared literally, applied to both the
expected project path and every discovered `layer.path()`, so neither a missing
project `.netsuke.toml` nor an unreadable directory fails discovery.

The normalizer is a seam rather than a bare function so the failure branch is
deterministic: real canonicalization failure depends on ambient filesystem state
a test cannot force portably. `FailingPathNormalizer` drives two new tests — one
asserting the error propagates unchanged, one asserting discovery still
succeeds. The existing `.` and `..` resolution cases are retained.

Extract the layer-collection concern into `discovery_layers.rs`; the added
policy pushed `discovery.rs` to 412 lines, past the 400-line cap.

* Install tracing before config resolution and relocate the capture harness (#292)

The instrumentation added for this issue never reached users. `init_tracing`
ran inside `configure_runtime`, after the merge, so every event emitted while
resolving configuration was dropped for want of a subscriber.

Install one global subscriber before `resolve_json_mode_or_exit` and
`merge_cli_or_exit`, behind a reloadable level filter. `--verbose` now selects
TRACE rather than DEBUG because the `NETSUKE_CONFIG` lookup is traced at that
level. The filter is re-applied once the resolved mode is known, and again after
the merge, so verbosity still follows the merged configuration without a second
subscriber. JSON mode maps to `LevelFilter::OFF`, keeping stderr to the
diagnostic document alone. The later `init_tracing` calls are gone:
`configure_runtime` reloads the filter, and `config_err_to_exit` simply logs,
since the subscriber already exists.

Colour is now gated on `stderr` being a terminal. Piped logs were carrying ANSI
escapes, which made them awkward to read and to grep.

Move the capture harness from `test_support` into the root crate as the
test-only `src/test_tracing_capture.rs`, and drop `tracing` and
`tracing-subscriber` from `test_support/Cargo.toml`; nothing else there used
them. This also fixes a coverage gap: `test_support` is excluded from the
workspace, so the harness's own tests never ran under `make test`.

Being `#[cfg(test)]` in the root crate, the harness is reachable from unit tests
only. The two integration tests that captured events in-process are replaced by
`tests/logging_stderr/config_tracing.rs`, which asserts against the real
binary's stderr: bounded fields for a successful explicit selection, the failure
kind for a missing and for a malformed file, absence of the raw path and the
parser's input, and no tracing at all in JSON mode.

* Satisfy Clippy on the new tracing setup (#292)

`startup_filter` is now `const fn`, and the two handle results use the `.ok()`
idiom the codebase already uses for `OnceLock::set` rather than `let _ =`, which
trips `clippy::let_underscore_must_use`.

`is_tracing_line` in the binary tracing tests no longer slices by index, which
`clippy::indexing_slicing` rejects; splitting on the first `-` and checking the
year is four digits reads better and cannot panic.

* Satisfy Clippy in the relocated tracing capture tests (#292)

These tests were never linted while they lived in `test_support`, which is
excluded from the workspace; moving them into the root crate brought them under
`make lint` for the first time and surfaced three restriction-lint violations.

Extract `spawn_reader` from the reader loop. That flattens the closure
`clippy::excessive_nesting` rejected and names the cloned barrier `gate`, so it
no longer shadows the outer binding.

Assert the nested-scope event with `first().is_some_and(..)` rather than
indexing, which `clippy::indexing_slicing` rejects.

* Harden config discovery diagnostics (#292)

Keep project-path comparison native through canonicalization so non-UTF-8
directories do not duplicate project layers. Exercise normalization and
JSON-mode tracing against resolvable, isolated fixtures.

Delay configuration tracing until the effective JSON mode is known while
preserving bounded failure diagnostics for human-mode resolution errors.
Clarify the fallback and unkeyed-hash privacy contracts.

* Document config tracing boundaries (#292)

Explain verbose selector and failure diagnostics, bounded path fields, and
JSON suppression for users.

Record the reloadable subscriber lifecycle, path-normalizer boundary, and
complete application lint exceptions for maintainers. Mark obsolete claims
in the completed config-selector plan as historical.

---------

Co-authored-by: leynos <leynos@troubledskies.net>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
leynos added a commit that referenced this pull request Sep 3, 2026
shared-actions#422 merged, and its merge commit is also the main tip carrying
#425, #427, and #428, so every reference in this repository now shares one SHA
instead of a placeholder and a second pin. A future bump moves them together.

That revision also closes the packaging lane's build-tree archive.
`rust-build-release` now forwards `cache-provider` to its nested `setup-rust`,
so the reusable packaging workflow passes `external` and no lane in this
repository archives a `target` tree any more. `use-sccache` stays enabled
there deliberately: the nested action is what installs sccache and exports the
backend's credentials, so disabling it would leave `RUSTC_WRAPPER` pointing at
a binary that was never installed.

The contract that held the shared actions to caller-owned caches now covers the
packaging step too, so the gap cannot reopen quietly the way it was opened: by
a nested pin nobody was watching.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY
leynos added a commit that referenced this pull request Sep 3, 2026
shared-actions#422 merged, and its merge commit is also the main tip carrying
#425, #427, and #428, so every reference in this repository now shares one SHA
instead of a placeholder and a second pin. A future bump moves them together.

That revision also closes the packaging lane's build-tree archive.
`rust-build-release` now forwards `cache-provider` to its nested `setup-rust`,
so the reusable packaging workflow passes `external` and no lane in this
repository archives a `target` tree any more. `use-sccache` stays enabled
there deliberately: the nested action is what installs sccache and exports the
backend's credentials, so disabling it would leave `RUSTC_WRAPPER` pointing at
a binary that was never installed.

The contract that held the shared actions to caller-owned caches now covers the
packaging step too, so the gap cannot reopen quietly the way it was opened: by
a nested pin nobody was watching.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY
leynos added a commit that referenced this pull request Sep 4, 2026
…664)

* Optimize Namespace runner caches

Give every Namespace job exactly one cache owner and make its effect
observable, following the estate adoption recipe's Phase 8 guidance.

- Mount one `nscloud-cache-action` volume per job and record its
  `cache-hit` output in the step summary, so a warm run is distinguishable
  from a cold one.
- Prefer explicit durable paths over command-dependent cache modes. The
  `rust` mode mounts Cargo's `target` directory, which duplicates sccache's
  role and breaks `cargo clean`; the `uv` and `bun` modes probe tools that
  are not always present.
- Hand the shared setup, Whitaker, and coverage actions `cache-provider:
  external` so they stop mounting a second owner for the same paths, and
  drop the `actions/cache` steps the volume now covers.
- Run sccache as the compiler-cache measurement layer, zeroing its counters
  before the build and emitting JSON statistics afterwards.
- Bound compilation and nextest workers to the profile's four vCPUs.
- Install Kani's Cargo front-end and verifier bundle from pinned, checksum
  verified prebuilt archives, and keep their Cargo, verifier, and Rustup
  homes on the volume so warm jobs skip the downloads entirely.
- Document the cache lanes and their owners in the developers' guide, and
  add workflow-contract tests that fail if a job loses its cache owner,
  gains a duplicate, or reintroduces a source build.

This squashes the branch's four incremental commits so the change rebases
onto main as one coherent unit.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Address CodeRabbit findings on the cache rollout

Split the merge gate, harden the prebuilt-tool installs, and tighten the
contracts that guard them.

- Move `build-test-windows` and the pull-request Windows recipe smoke job
  into `.github/workflows/ci-windows.yml`, invoked as a reusable workflow.
  `ci.yml` had grown to 574 lines, well past the 400-line file limit
  AGENTS.md sets. GitHub does not expose the `env` context to a reusable
  workflow's inputs, so the caller repeats its version pins as literals and
  a contract test holds the two copies equal.
- Install `cargo-orthohelp` after `rust-build-release` in the packaging
  workflow. That action provides the checksum-verified `cargo binstall`, so
  the earlier placement failed on any runner without a preinstalled
  `cargo-binstall`. Nothing before the help-generation step needs the tool.
- Pass `--disable-strategies compile` to every `cargo binstall` call, so a
  missing prebuilt release fails the job instead of silently compiling.
- Restrict the packaging build job and its three callers to `contents:
  read`. The pinned action chain builds and uploads artefacts; it neither
  publishes packages nor exchanges an OIDC token.
- Give Kani's front-end and verifier bundle version-qualified directories on
  the cache volume, so raising the pin in `tools/kani/VERSION` cannot be
  satisfied by a stale binary an earlier run left behind.
- Fold the yamllint and actionlint `actions/cache` steps that arrived with
  main into the Namespace volume, and reuse the cached actionlint only when
  it reports the pinned version.
- Contract tests: reject any `cargo install` form that would compile
  `cargo-orthohelp`, whatever flags precede the crate name; require the Kani
  cache to be mounted before Kani is installed; bind each archive's
  `sha256sum --check` to that archive's own extraction; and require every
  cache summary to carry `if: always()` and report the volume's `cache-hit`.
- Correct the developers' guide punctuation and document the split, the
  version-qualified Kani layout, and the caller-owned Whitaker cache.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Clear the empty Whitaker mount before installing

The Linux merge gate failed with `git pull failed: fatal: not a git
repository` while installing Whitaker. Mounting the cache volume creates
`~/.local/share/whitaker` even on a cold run, and `whitaker-installer`
0.2.7 chooses between cloning and pulling on directory existence alone, so
it pulled against a directory that held no repository.

Add a `Prepare Whitaker cache directory` step to both Whitaker jobs that
deletes the data directory when it is not a Git repository, and record the
removal condition in the developers' guide. Correct the Windows cache path
at the same time: the installer clones into
`~/AppData/Roaming/github/whitaker` there, so the previous
`~/.local/share/whitaker` entry cached nothing.

Split the two workflow-contract modules that crossed the 400-line limit,
extracting the actionlint installer constants and the Kani cache contract
into their own modules, and factor the long helpers the Clippy and Pylint
gates flagged.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Mark the shared-actions pin as a placeholder

The pinned revision is the head of shared-actions pull request 422, which
is still open. Say so in the developers' guide and state the removal
condition, so the pin is not mistaken for a merged commit.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Hold the formatter installers to a prebuilt release

`cargo-binstall`'s default strategy list ends in `compile`, so a missing
prebuilt artefact would be compiled in CI without anyone noticing. Both
formatter jobs already pass `--disable-strategies compile`; require it in
the workflow contract, and reject `cargo install` in the same step.

Start the CI workflow from an empty token and let each job opt in, rather
than inheriting the repository default.

Move the formatter installer contract into its own module: adding the
assertion pushed `ci_lint_test.py` past the 400-line limit. `SETUP_RUST_JOBS`
moves to `workflow_loading.py` so both suites read it from one place.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Move Linux CI to Ubicloud and Windows to GitHub-hosted runners

Ubicloud offers Linux runners only, so the estate splits along one line: the
Linux jobs that block a developer run on `ubicloud-standard-2-ubuntu-2404`,
and everything else runs on a GitHub-hosted runner. Windows and macOS have no
Ubicloud image; the metadata, publication, and delayed-comment jobs are
API-bound, and Ubicloud has no single-vCPU shape to make them cheaper. The
Ubuntu 22.04 compatibility lane takes the `-ubuntu-2204` label so the older
glibc it exists to exercise is still the one it runs on.

Every Linux job starts at the two-vCPU shape. `ubicloud-standard-4` is the
ceiling, not the default, and no escalation evidence exists yet, so the worker
bounds now derive from one named lane constant per shape rather than the four
workers the retired Namespace profiles supplied.

Ubicloud destroys the runner VM after each job, so the cache-volume design has
no counterpart: warm state arrives only through archives. Each lane's cache
steps move into a composite action that renders its keys once and owns a
disjoint path set, so restore, save, and the observation summary cannot drift
apart and no path gains a second owner. Restores precede every install; saves
happen only on a push to `main` where that key missed, which is why `ci.yml`
gains a trunk trigger and the Kani job now runs on it. One job writes each key
family and the rest restore only.

Linux keeps sccache on the GitHub Actions backend and exports the two runner
variables itself, because disabling the shared action's sccache also disables
the step that normally publishes them. The local-directory fallback is wired
behind a single repository variable, so exactly one backend is ever active.
Windows keeps its compiler cache in an explicit workspace directory under
`actions/cache`, keyed by OS and architecture so a Linux archive can never
restore onto it.

The Namespace contract modules are replaced by runner-placement, cache
ownership, write-policy, and compiler-cache suites over the same shared pure
validators, with the Hypothesis properties retargeted onto duplicated cache
ownership, oversubscribed worker counts, and save conditions that do not name
a trunk push.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Make sccache the only owner of compiler output on every lane

Cargo's build tree is now archived nowhere. sccache holds every build shape
this repository produces, and the shapes coexist in one store because sccache
hashes the flags that separate them, so a `target` archive would be a second
owner of the same bytes and would be invalidated far more often than it
helped. `cache-provider: external` was already keeping the shared actions'
`target` caches switched off; the change is that the coverage build, the
Netsukefile compatibility build, and the packaging build now compile through
sccache too, rather than leaving whole shapes uncached. Sizing rises to 4 GB
because one store now holds two shapes.

One gap stays open and is recorded rather than papered over: the shared
`rust-build-release` action nests an older `setup-rust` revision that caches
`target/${BUILD_PROFILE}` unconditionally and exposes no passthrough, so the
packaging lane still archives a build tree. Closing it needs a shared-actions
change, so the packaging job sets the wrapper and installs no second sccache
beside the nested one.

Ubicloud's transparent cache intercepts `actions/cache` at v6.1.0, so one
action at one pin now serves every lane and the deprecated `ubicloud/cache`
fork is gone. That also retires the rule that the fork must never appear in a
job which can reach a GitHub-hosted label.

sccache keeps its GitHub Actions backend, whose objects land in Ubicloud's own
store. Reaching it needs the two Actions cache variables, and the runner hands
those to JavaScript actions rather than to shell steps, so every lane exports
them through a dedicated action immediately after checkout. Ordering is the
substance of that contract: `--zero-stats`, `--start-server`, and the first
wrapped `rustc` all start the server, and a server started without those
variables stays on local disk for the whole job and reports zero compile
requests, which this repository's pilot has already suffered once.

The export also runs on the Windows lanes, which the estate directive says do
not need it. It costs a second and removes the same silent failure mode, and
Windows moves to the same backend here, so the guard is worth more than the
step it saves.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Cite the tracking issue for the packaging lane's build-tree archive

The packaging lane is the one place that still archives a Cargo build tree,
because the shared build action nests a setup-rust revision that caches it
unconditionally. Both the call-site comment and the developers guide said only
that the fix belonged to shared-actions, which left a reader no way to find out
whether it had happened. Name leynos/shared-actions#426 and say what to do when
it lands, so the note retires itself rather than ageing into folklore.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Correct the Ubicloud App grant note

The guide claimed the App did not yet cover this repository. It does; the
account-wide installation was already in place. The claim came from reading the
Ubicloud repository listing as a grant inventory, but that listing names only
repositories which have already run a job, so any repository awaiting its first
Ubicloud run is absent from it either way. Say what the prerequisite is, say
why the listing cannot answer it, and point at the symptom that would justify
rechecking the console.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Measure coverage once per commit

On a push to `main` the merge gate and the trunk coverage job both ran an
instrumented build of the same commit, and both wrote the ratchet baseline, so
the estate paid twice for one measurement and gave a cached artefact two
owners. The gate now measures coverage only on a pull request, where the
changed-line check is the sole consumer of the report. The trunk job keeps the
upload and becomes the single baseline writer.

The other half of the deduplication rule does not apply here, and the
enumeration that shows why is now in the guide. The gate's suite is a strict
superset of the coverage run rather than a subset of it: the coverage action
invokes nextest with default features and default targets and does not deny
warnings, so folding the gate into it would stop exercising the
`legacy-digests` feature, stop compiling the bench targets, stop denying
warnings in the test tree, and drop doctests entirely. Keeping both on a pull
request buys real coverage; keeping both on the trunk bought nothing.

A contract now holds the gate to the full workspace, every target, every
feature, warnings denied, and the doctest pass, so a later edit cannot narrow
it into a duplicate of the coverage run and make the fold look safe in
retrospect.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Execute the Linux suite once, inside the coverage run

The merge gate compiled and ran the workspace twice per pull request: once
uninstrumented through `make test`, then again instrumented to measure it. The
instrumented run now does both, so the lane pays for one compile instead of
two, and the uninstrumented pass is gone.

That is only sound while the instrumented invocation is as broad as the pass it
replaced. The coverage action defaults to nextest with default features and
default targets, which would have quietly retired the `legacy-digests` tests
and stopped compiling the bench targets, so both coverage callers now ask for
every feature and every target. Warnings stay denied through setup-rust's
`rustflags` input rather than a job-level RUSTFLAGS, which the Polonius
contract forbids, and `cargo llvm-cov` appends its instrumentation to whatever
it finds. Doctests are the one thing an instrumented run cannot execute at all,
so a cheap uninstrumented pass follows it on every event, including the trunk.

The trunk coverage job gains the same flags, so the baseline it writes is
measured over the set the ratchet later checks against rather than a narrower
one.

`build-test` keeps its name, so the required context is unchanged; the fold
moved work into the job rather than replacing it. The worker bounds move with
the work: the instrumented run reads cargo's and nextest's own variables, so
the two Make variables the removed step consumed are replaced by the ones that
now take effect, rather than left behind as decoration.

The workflow is written against the `all-features`, `all-targets`, and
`doctests` inputs that leynos/shared-actions#428 adds. Until that merges the
pinned action ignores them with an "Unexpected input(s)" warning, so this is
not exercisable in CI before the repin.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Take the doctest pass from the coverage action

Repins `generate-coverage` to the merge of shared-actions#428, which supplies
the `all-features`, `all-targets`, and `doctests` inputs the single-execution
rule was written against. Every other shared action stays on the #422
placeholder, which is still unmerged; the two pins are independent actions with
no shared surface.

The merged action turns out to run the doctests itself, uninstrumented and
under the same feature selection as the instrumented run, for exactly the
reason this repository needed them: so one coverage job can be a lane's only
test execution. The bespoke `make doctest` step is therefore removed rather
than kept beside it. Two statements of the feature selection could drift; one
cannot, and widening the instrumented run in future now widens the doctest run
with it.

`make test` keeps composing both passes for local use, and a contract holds its
flags equal to what CI drives, so a contributor's run still matches the gate.

Checked before repinning rather than after: the merged action still accepts
`cache-provider`, so the caller remains the single cache owner, and its own
cargo cache no longer lists a `target` tree at all.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Pin every shared action to one merged revision

shared-actions#422 merged, and its merge commit is also the main tip carrying
#425, #427, and #428, so every reference in this repository now shares one SHA
instead of a placeholder and a second pin. A future bump moves them together.

That revision also closes the packaging lane's build-tree archive.
`rust-build-release` now forwards `cache-provider` to its nested `setup-rust`,
so the reusable packaging workflow passes `external` and no lane in this
repository archives a `target` tree any more. `use-sccache` stays enabled
there deliberately: the nested action is what installs sccache and exports the
backend's credentials, so disabling it would leave `RUSTC_WRAPPER` pointing at
a binary that was never installed.

The contract that held the shared actions to caller-owned caches now covers the
packaging step too, so the gap cannot reopen quietly the way it was opened: by
a nested pin nobody was watching.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Install the two tools that publish no usable binaries

The first genuinely cold run found what a warm cache volume had been hiding:
neither mdtablefix nor cargo-orthohelp can be installed from a prebuilt
release, and with the compile strategy correctly disabled both simply failed.
The policy was right; the artefacts were never there to test it against.

mdtablefix publishes perfectly good tarballs, but its binstall metadata sets
`bin-dir = "."`, so resolution fails before it reaches them
(leynos/mdtablefix#458). The Linux lane now takes the tarball directly and
verifies it against a digest pinned in the action and cross-checked against the
release's own sidecar, the way actionlint is already installed. No Windows
binary is published at all, so that lane compiles the tool once per cache
generation into a directory the tool cache owns and the product never shares.

cargo-orthohelp has no binaries on any platform (leynos/ortho-config#479), so
the packaging lane keeps binary-only strategies first and falls back to a
source build on a genuine miss, into a dedicated target directory cached under
a key carrying the tool version. That lane is the one place a save is not
restricted to a push on `main`: no trunk event reaches the release workflow, so
the run that builds the tool has to be the run that publishes it, and the key
is content-addressed by version so concurrent writers agree.

Both exceptions are recorded with the issue that retires them. The contract
counts `cargo install` occurrences rather than pattern-matching them, so a
second source build cannot shelter behind the first, and the packaging contract
admits the fallback only in its exact guarded form.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Resolve the orthohelp build directory in the shell, not in env

The macOS packaging job failed the moment it reached the source-build fallback,
for two reasons that only bite outside Linux. GitHub does not expand `~` in an
`env:` value, so the target directory was a literal tilde rather than a path
under the home directory; and `mkdir --parents` is a GNU long option that the
BSD coreutils on macOS reject outright, which is where the exit code 64 came
from. Resolving the path from `$HOME` inside the script and using `-p` fixes
both, and the installer action drops the same long option so it cannot repeat
the failure if a macOS lane is ever added.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Size the gate to its workload and stop fighting GitHub's cache limits

Two runner-shaped failures, two different answers.

The merge gate lost its VM 16 minutes into the instrumented build on the
two-vCPU shape: every later step reported a null conclusion and GitHub served
no log, which is a machine disappearing rather than a step failing. The
instrumented run compiles the whole workspace with every feature and every
target, so memory is the plausible cause, and the recipe's escalation path
allows `-4` on exactly that evidence. But the cause was inferred, not measured,
so the job now samples used memory every 15 seconds and prints the peak into
its summary. If the peak comes back well under 6 GB the shape goes back to
`-2`; the note saying so lives beside the label rather than in this message.

The Windows lanes were not short of anything. They were being rate-limited:
the gate recorded 643 failed cache writes out of 643 on sccache's GitHub
Actions backend, and the packaging build 68. Those lanes now keep the compiler
cache in a workspace directory that the cache action owns, under a rolling key
with a prefix restore-key, and set no backend flag at all. The Ubicloud and
macOS lanes keep the backend, where it works, so the credential export narrows
to the lanes that still need it.

The packaging lane also gains the sccache installer it never had. Setting
`RUSTC_WRAPPER` to a binary nobody installed is the whole of "sccache: error:
failed to spawn Command", and a contract now ties the wrapper to an installer
that precedes the build.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Escalate the trunk coverage job alongside the gate

The trunk coverage job runs the same instrumented workload that lost the
merge gate's runner, and it is the writer every warm run reads from, so a VM
lost there would leave the whole estate cold rather than just failing one
check. It moves to the same shape on the same evidence instead of waiting to
reproduce the failure on `main`.

The sampler moves into a composite action rather than being pasted into a
second workflow. It was already the kind of thing that drifts: two copies of a
background loop, a PID handed between steps, and a summary format that only
matters if both copies agree. One implementation also gives the contract one
thing to assert.

Both jobs now declare the same lane size and derive their worker bounds from
it, and a contract holds the two equal, so a future resize cannot move one and
leave the other with the failure this addressed. The contracts split along the
same seam: placement says which provider runs a job, shape says how large that
runner is and requires the measurement that lets the escalation be reversed.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Pin the shared actions to the Whitaker digest fix

The Windows gate was rejecting a correct Whitaker archive: GNU `sha256sum`
prefixes its output with a backslash when the path it was given contains one,
so the comparison failed on the digest rather than on the download. The shared
action now hashes the archive from standard input, where there is no path to
escape, and all twenty-three references move to that revision together.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Probe before installing orthohelp, and log the cache evidence

Two things the first green-ish run exposed.

The orthohelp step had no version probe, alone among the installers here. The
cache restores `~/.cargo/bin`, and `cargo install` refuses to overwrite a
binary that is already present, so a warm run walked into the source-build
branch and failed on a cache hit. Probing first is what every other installer
in this repository already did; this one should have from the start.

The cache observations and the memory peak were written only to the job
summary, which the jobs API does not expose, so the evidence the exit gate
asks for could be read in a browser and nowhere else. They are teed to the log
as well now, which is where anyone reconstructing a run actually looks.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Route sccache through Ubicloud's proxy, and stop caching Windows releases

Three findings from the run, and the contracts that should have caught two of
them.

The compiler cache was writing to GitHub the whole time. The export published
`ACTIONS_RESULTS_URL`, which is GitHub's v2 results service, and sccache 0.16
prefers v2 whenever the service switch is set, so every object resolved past
Ubicloud's local proxy: 5310 requests, zero hits, one write error per miss, and
92 objects in GitHub's store rather than Ubicloud's. The export now publishes
the proxy address the runner advertises and clears the v2 switch, so sccache
uses the endpoint the proxy actually serves.

The Windows packaging lane stops using a compiler cache at all. sccache
re-spawns rustc there with the aarch64 target's whole `--extern` and `-L` list
and exceeds the operating system's command-line limit, which nothing in this
repository can shorten. Release builds are infrequent, so that lane runs
uncached rather than unreliably; the Windows merge gate keeps its local
sccache.

Two contracts were quietly blind. The cache-step matcher looked for `/cache/`
and so missed the combined `actions/cache@` form entirely, meaning a second
owner declared that way would have passed every ownership check; and forbidden
paths were compared exactly, so `target` was rejected while `target/debug`
sailed through. A lane that lost its cache steps altogether also satisfied
every contract vacuously. All three now fail rather than pass in silence.

The Windows cache action takes its toolchain as a declared input instead of
reading a variable its caller happened to set, which would have silently
shared one key across toolchains the first time a new caller forgot it.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Pin the shared actions to the Windows tar-path fix

The Whitaker digest verified after the last repin, but extraction then failed:
GNU tar in Git Bash reads the colon in a Windows drive letter as a remote host
separator, so a staging directory of `D:\a\_temp/...` became an attempt to
connect to host `D`. The shared action now converts the path with `cygpath`
before unpacking, and all twenty-three references move to that revision
together.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Measure disk, discard the instrumented tree, and fix an inverted guard

The Windows packaging lane still compiled through sccache after being told not
to. GitHub's `&&`/`||` yields the last truthy operand and an empty string is
falsy, so `platform == 'windows' && '' || 'sccache'` set the wrapper on exactly
the platform it meant to exempt, and the build then failed looking for an
sccache nobody had installed. Both guards are negated now, with the non-empty
value on the `&&` side, and a contract pins that shape because the mistake is
invisible on reading.

The escalation story turns out to be about disk, not memory. The gate peaked at
3,640 MiB against 16 GB, so memory was never the constraint; a sibling
repository's silent death on the smaller shape was disk exhaustion from a
second target tree. The sampler now records disk used and free alongside
memory, both jobs delete the instrumented tree once the report exists with
`df -h` either side, and the guide says the return to the smaller shape turns
on the disk figures rather than the memory one.

Two contracts close gaps a sibling repository fell into: the compiler-cache
backend flag must accompany the wrapper on every Ubicloud lane, since the
shared setup action sets neither, and the credential export must precede both
the toolchain setup and any step that starts the server. On Ubicloud the runner
re-injects the v2 service variables into every action step, so a server started
inside the setup action binds GitHub's service whatever the export said.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Bind cache gates to the keys they publish

Action the outstanding review findings on the Ubicloud runner move, and
close three gaps the contracts did not previously reach.

Wiring:

- Bump every shared-actions reference from 1f303985 to c6125f19. The two
  revisions differ only in setup-rust's rustc-wrapper export, which every
  caller here disables, so this unifies the pin without changing behaviour.
- Give the Kani cache action an explicit `runner-image` input. It read
  NETSUKE_RUNNER_IMAGE under `set -u` while declaring no dependency on it,
  so a caller without the workflow-level variable would have aborted while
  rendering the key rather than failing on a missing input.
- Rename that action's hit input from `cache-hit` to `kani-hit`, so every
  composite save is gated on an input named for the key it publishes and the
  write policy can derive the expected name rather than pattern-match it.
- Fail the sccache credentials action when SCCACHE_GHA_ENABLED is true and
  either credential is empty. That combination is otherwise silent: the
  server falls back to local disk and the job still passes.
- Derive coverage-main's SCCACHE_GHA_ENABLED from the same repository
  variable the merge gate reads, and restore the gate's local store on the
  local-directory arm. Pinned true, the trunk baseline would have compiled
  cold on every run with nothing failing.
- Read coverage-main's cache-key toolchain from rust-toolchain.toml instead
  of restating the nightly date. The Setup Rust literal stays, because the
  Polonius contract checks it against that file; both are now anchored to
  one source and cannot drift apart.
- Carry the pinned rust-build-release revision in the cargo-orthohelp cache
  key. The entry owns ~/.cargo/bin, which also holds the cargo-binstall that
  action provisions, so a bump to the action must turn the entry over.
- Give the packaging job a `timeout-minutes`. It had none, and its runner is
  caller-selected, so release.build-linux could leave a Ubicloud VM running.

Contracts:

- Bind each cache save's hit gate to its own key, for composite and inline
  saves alike. Matching any `-hit` input let a tools save gate on the
  registry result, which is the mis-gate the write policy exists to prevent.
- Derive the inline-save parametrization from KEY_WRITERS.
- Reject a top-level disjunction in a trunk-only save condition.
- Require a listed read-only lane to declare a restore step rather than
  skipping when it has none.
- Assert the statistics reset precedes the first compile and the report
  follows the last, on every lane declaring both.
- Cover the caller-selected Linux package job's timeout.
- Enforce the Kani `runner-image` input at the action and at every caller.
- Scan composite action inputs, not only `run` scripts, for a second Linux
  suite execution.
- Promote the inline job inventories into cache_contract_data, so a lane
  added there cannot go unchecked.

Structure and documentation:

- Split the runner-placement mutations and the sccache compile-step
  recognizer into sibling data modules, keeping every file inside the
  400-line limit.
- Give the shared cache and invariant helpers full NumPy docstrings.
- Document the guarded cargo-orthohelp fallback as the three stages the lane
  actually runs, rather than the single binstall line.

Forwarding NEXTEST_TEST_JOBS to `dev-test` is tracked separately as #671; it
is a local-workflow change that no CI lane exercises.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Flatten the second-execution scan into helpers

CodeScene flagged the widened forbidden-command scan on three biomarkers at
once: cyclomatic complexity 9, two blocks of nested conditional logic, and a
nesting depth of 4. Scanning `with` values as well as `run` scripts added a
fourth loop inside the existing three.

Split the traversal into `_step_offenders`, `_is_scannable_job` and
`_workflow_offenders`. The contract is unchanged and the exemption comment
moves to the predicate that applies it.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Run the release packaging lanes without a compiler cache

The Windows lane already ran uncached, because sccache re-spawns rustc with
the aarch64 target's whole `--extern` and `-L` list and exceeds the operating
system's command-line limit. The other lanes were only nominally cached: their
server starts inside the nested `setup-rust`, whose `mozilla-actions/
sccache-action` re-exports `ACTIONS_CACHE_SERVICE_V2` and GitHub's own results
address to `GITHUB_ENV` as its last act. On a Ubicloud runner that sends every
write past the cache proxy to GitHub, where it is rate-limited and lands in no
store this repository reads. There is no credentials export in this workflow to
clobber, so the lane was paying sccache's overhead for nothing.

Reproducing the merge gate's export, install and run-step start sequence for a
workflow that runs on tag pushes and the dry run only would not pay back, so
the lane now runs uncached on every platform: no `RUSTC_WRAPPER`, no backend
flag, no installer, and `use-sccache: 'false'` into the nested action.

That also removes the negated `platform != 'windows'` expressions. Getting that
negation backwards cost a release build once; requiring the variables to be
absent outright removes the expression and the whole class of mistake with it.

Correct the credentials action's own explanation while here. It claimed the
runner withholds reserved variables from shell steps. Measurement on
shared-actions runs 33854048777 and 33854213968 shows composite `run` steps do
see them, and that the real failure is the clobber described above. The
developers guide carried the same superseded reading.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Reject a cache-save disjunction at any depth

Four findings from the second review round.

`is_trunk_only_save` rejected a top-level `||` but accepted the same
disjunction in parentheses, which means exactly the same thing while
presenting no top-level operator to find. It now rejects `||` anywhere
outside a quoted literal. Rejecting outright, rather than deciding which
disjunctions are safe, is deliberate: every cache-save condition here is a
conjunction of equality tests, so nothing legitimate needs one, and a
predicate that tried to tell safe from unsafe would be a second, subtler
thing to get wrong. The parenthesised form joins the mutation set, and the
named regression test becomes three cases: bare, parenthesised, and nested
inside a conjunction.

The mdtablefix `build-dir` check compared substrings, which was wrong in
both directions: it accepted the relative `target/tool-build` and its
Windows spelling, which are the product's own tree, while rejecting
`build/target-cache`, whose first component merely shares a prefix. It also
accepted `.`, the workspace root, despite the whole point being a dedicated
directory. `is_dedicated_build_dir` now compares path components under both
POSIX and Windows flavours, and eleven parametrized cases pin the ones the
substring test got wrong.

Three workflow sweeps globbed `*.yml` only. GitHub Actions accepts `.yaml`
just as readily, so a workflow using it would have slipped past the cache
ownership, Kani and runner placement contracts unexamined.

Spelling: this repository is en-GB-oxendict, which takes the `-ize` forms.
The docstrings added on this branch used `normalised`, `recognised` and
`unrecognised`.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Let a warm-run dispatch reach every cache

The runner migration's exit gate measures warm cache behaviour by dispatching
the gate workflows on `main`. Two lanes were unreachable that way, so two of
the five key families would have had no warm evidence short of merging
something.

`kani-smoke` excluded dispatches on the grounds that a manual run has no
verification work to gate. True, but it is also the only job that touches the
Kani cache, so excluding it left that key measurable only by a real push. It
now runs on every trigger.

`coverage-main.yml` had no dispatch trigger at all. It now has one.

Neither can publish a generation. Every save in this repository gates on
`github.event_name == 'push'` together with `github.ref == 'refs/heads/main'`,
and `coverage-upload` declares no save at all. A new contract holds that
directly rather than by inference: for every workflow that accepts a dispatch,
each composite writer flag and each inline save must name the push event.
Naming the ref alone would not be enough, because `github.ref` is
`refs/heads/main` on a dispatch against the trunk too. Dropping the push
predicate from the Kani action's writer flag makes it fail.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Pin shared-actions to the Windows extraction fix

The Windows merge gate has been failing since the lanes moved to
`windows-latest`. `install-whitaker` chose its archive extractor by probing
what `tar` is, and on that image the answer is GNU tar, which cannot read the
`.zip` installer asset. The step's `shell: bash` is Git Bash, whose PATH puts
MSYS2's tar ahead of the Windows system directory, so the probe never saw the
bsdtar its own comment assumed. That held only on the Namespace image this
branch migrated away from.

leynos/shared-actions#448 fixes it upstream, choosing the extractor by the
resolved asset extension instead, and is now merged. Repin all 23 references
from c6125f19 to that merge commit. The new revision also carries #440 and
#445, so `setup-rust` now restores the cache service variables that
`mozilla-actions/sccache-action` overwrites; nothing here depends on that yet,
since every caller keeps `use-sccache: 'false'`.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Split the dispatch contract into named steps

CodeScene flagged the new contract as a Complex Method, and fairly: it walked
the composite actions, the inline saves and the dispatchable workflows in one
body, with the assertion at the end.

Each walk becomes a helper that returns labelled conditions, so the test reads
as its three parts: something must accept a dispatch, saves must exist, and
every one of them must name the push event. The contract is unchanged, and
dropping the push predicate from the Kani action still fails it.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Give the isolated-build tests a Windows budget

With the Whitaker installer fixed, the Windows gate reaches its `Test` step,
and `harness_compiles_under_a_split_build_dir` terminated there at 300.081s
with the other 2,503 tests passing. The immediately preceding run finished the
same test inside the budget, which is what marginal looks like rather than
broken.

These two tests each spawn a complete isolated Cargo build. The Windows gate
now runs on a four-vCPU GitHub-hosted runner rather than the Namespace profile
it used before, and that shape's compile throughput does not fit the 300s
default. This is the genuine external-resource constraint the configuration's
own policy reserves a targeted override for, so it gets one with the rationale
written down, rather than a retry.

Scoped to Windows. On Ubicloud these finish in a fraction of the budget, and
widening the timeout there would blunt the hang detection the default exists
to provide.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Hold the worker bounds and the lane shapes under contract

Three gaps the review round found, none of them in the workflows themselves.

The Makefile forwards two separate worker bounds to `cargo nextest run`, and
nothing held either in place. `NEXTEST_BUILD_JOBS` limits the compile that
precedes the run and `NEXTEST_TEST_JOBS` limits the test processes, so a lane
on a small runner can bound each without oversubscribing the other. Dropping
either silently returns that half to nextest's default of one worker per core,
which is what exhausted a two-vCPU runner in the first place. The Makefile
contract now requires both, and requires the test bound to appear after
`nextest run` rather than merely somewhere in the recipe, where it would read
as configured while bounding nothing.

The developers guide still described one two-vCPU merge gate deriving every
worker bound from a single count. That has not been true since the instrumented
lanes were escalated. It now carries a table of the six lanes with their
runners and actual variables, and separates the two four-vCPU instrumented
lanes from the two-vCPU Kani, Netsukefile and packaging lanes. The escalation's
justification is recorded with the evidence rather than asserted: peak volume
usage of about 80.6 GiB across three runs, against `ubicloud-standard-2`'s
whole 72 GB volume, with memory never above 2,668 MiB of 16 GB.

Two of the guide's universal claims had acquired an exception. The release
packaging lane records no cache observations, because its entry is
content-addressed and reached only by tag pushes and the dry run, so there is
no warm-versus-cold trend to report; and it uses no compiler cache at all, so
it must stay free of `RUSTC_WRAPPER`, `SCCACHE_GHA_ENABLED` and `SCCACHE_DIR`
rather than merely setting them empty. Both exceptions are now stated where
the rule is.

Also rename the dispatch contract's first helper to say what it returns, and
give both helpers the docstrings their new status as a module interface earns.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Bind the worker bounds to the command they bound

Four review findings, one of which weakened a contract I had just added.

`target_recipe` returns the recipe's lines joined together, so checking that
`$(NEXTEST_TEST_JOBS)` appears after `nextest run` in that string would accept a
bound sitting in an unrelated later command, reading as configured while
bounding nothing. Both bounds are now asserted against the line that invokes
`nextest run`, which is the thing they have to be arguments to.

Composite actions were discovered with a single-level glob, so one nested a
directory deeper would have bypassed both the save-gating policy and the
source-build scan without any test noticing. Both now recurse.

Two `isinstance` checks become structural pattern matches, per the repository's
Python guidance. `_accepts_a_dispatch` keeps returning False for the scalar and
list spellings of `on`, neither of which can carry the trigger's inputs.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY

* Extract the nextest worker-bound assertions

Adding the two bounds took
`behavioural_make_test_composes_the_nextest_and_doctest_passes` to exactly the
70-line threshold CodeScene enforces.

The bounds check is a self-contained question about one command, so it becomes
`ensure_worker_bounds_reach_nextest`, which carries the reasoning that was
previously two comment blocks in the middle of the test: why there are two
variables rather than one, and why the assertion is made against the invoking
line rather than the joined recipe. The test drops to 55 lines and reads as the
sequence of properties it checks.

Claude-Session: https://claude.ai/code/session_01QrjNTnTwM7FmWXe5KFPMPY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Roadmap A pull request originating from a roadmap item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants