Skip to content

Split cuprum/context.py into a context/ package (#116) - #157

Merged
leynos merged 27 commits into
mainfrom
issue-116-split-context-package
Jul 24, 2026
Merged

Split cuprum/context.py into a context/ package (#116)#157
leynos merged 27 commits into
mainfrom
issue-116-split-context-package

Conversation

@leynos

@leynos leynos commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

This branch splits the 763-line cuprum/context.py into a cuprum/context/ package with every module under 400 lines, preserving the public API surface unchanged.

Closes #116.

The module mixed four concerns with different audiences and change cadences. The split follows the seams named in the issue:

Review walkthrough

  • The code moves verbatim apart from import plumbing; review the new module boundaries and __init__ surface first, then skim the per-module imports.
  • docs/developers-guide.md documents the package layout and where new context features belong.

Validation

  • make check-fmt: pass
  • make lint: pass (no module over 400 lines; largest is registration.py at 372)
  • make typecheck: pass
  • make test: pass (727 passed, 50 skipped; the public-API, context, env-overlay, and stateful registration suites pass unchanged; Rust suite 4 passed)
  • make markdownlint: pass
  • coderabbit review --agent: invoked after the final push; the CLI began analysis but returned no terminal findings report

Notes

This branch is stacked on #156 (#113) per the issue's coordination note, so registration.py lands already de-duplicated; it should be rebased once #156 merges. The wheel-build snapshot reflects the new file list.

References

@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 2500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

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

Walkthrough

Split the former cuprum/context.py module into a package containing context types, environment helpers, ContextVar state, and registration handles. Re-export the public API, update packaging and documentation, revise benchmark ratcheting, preserve Rust error conversion details, and centralize concurrent pipe-test handling.

Changes

Execution context package

Layer / File(s) Summary
Context domain and environment overlays
cuprum/context/core.py, cuprum/context/env_overlay.py, cuprum/unittests/test_context.py
Define immutable context types, allowlist enforcement, hook and timeout handling, environment overlay merging, and validation tests.
Context state and registration lifecycle
cuprum/context/state.py, cuprum/context/registration.py, cuprum/unittests/test_token_registration_stateful.py
Provide ContextVar access and scoped registration handles with token restoration and nested environment-detach coverage.
Package exports and distribution documentation
cuprum/context/__init__.py, cuprum/unittests/__snapshots__/test_maturin_build.ambr, docs/adr-006-context-package-split.md, docs/developers-guide.md
Expose the split implementation, update wheel contents, and document module ownership and registration protocols.

Benchmark ratchet measurement

Layer / File(s) Summary
Deterministic benchmark profile
benchmarks/*, cuprum/unittests/test_benchmark_ci_ratchet.py, cuprum/unittests/test_ci_benchmark_ratchet_profile.py
Configure ten benchmark runs, sort selected scenarios deterministically, validate scenario metadata, reject the previous profile version, and update tests.
Benchmark investigation record
docs/debugging/*, docs/cuprum-design.md, docs/developers-guide.md, docs/users-guide.md
Record investigation hypotheses and evidence, and describe the revised benchmark measurement and compatibility procedures.

Rust pump I/O and error handling

Layer / File(s) Summary
Rust OS error conversion
rust/cuprum-rust/src/errors.rs, docs/developers-guide.md
Document preservation of specific Python I/O exception types during Rust error conversion.
Concurrent pump test plumbing
tests/helpers/stream_pipes.py, cuprum/unittests/test_rust_splice.py, cuprum/unittests/test_rust_streams.py, tests/behaviour/test_rust_streams_behaviour.py
Run pipe writers and readers concurrently around the native pump and reuse shared helpers across unit and behaviour tests.

Possibly related PRs

Suggested labels: Issue

Poem

Four modules clear the crowded room,
Context and overlays bloom.
Tokens restore each scoped state,
Ten-run benchmarks calibrate.
Pipes flow; errors keep their code.

🚥 Pre-merge checks | ✅ 18 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several benchmark, stream-helper, Rust, and docs edits sit outside the context-package split. Move unrelated benchmark, Rust, and helper changes into separate PRs, or justify them in #116.
User-Facing Documentation ⚠️ Warning users-guide.md documents the benchmark ratchet skip, but omits that incompatible baselines also write a skip report, which the code does. Update the CI build commands section to say incompatible baselines produce a skip report, not just a skipped comparison.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the context package split and includes the linked issue reference (#116).
Description check ✅ Passed The description tracks the same context-package split and stays on-topic.
Linked Issues check ✅ Passed The package split, re-exports, module boundaries, and under-400-line limits match issue #116.
Docstring Coverage ✅ Passed Docstring coverage is 99.04% which is sufficient. The required threshold is 80.00%.
Testing (Overall) ✅ Passed Substantive coverage exists: property tests, a Hypothesis state machine, real subprocess env tests, and observe/benchmark checks would fail on plausible broken implementations.
Developer Documentation ✅ Passed docs/developers-guide.md documents the new context package layout, and ADR-006 records the split decision; no execplan or roadmap item applies.
Module-Level Documentation ✅ Passed All changed Python modules carry top-level docstrings, and the new context package modules explain their roles and relationships to sibling modules.
Testing (Unit And Behavioural) ✅ Passed PASS: Unit tests cover invariants, edge cases, and errors; BDD tests hit public context and subprocess env boundaries, not private helpers.
Testing (Property / Proof) ✅ Passed Hypothesis suites and a state machine already cover allowlist, timeout, env-overlay and token-restoration invariants.
Testing (Compile-Time / Ui) ✅ Passed PASS: Rust compile-time coverage already uses trybuild UI tests, and the wheel-build snapshot is focused, redacted, and paired with semantic assertions.
Unit Architecture ✅ Passed PASS: Query helpers stay read-only; mutation and side-effects are isolated to registration handles and subprocess-boundary helpers.
Domain Architecture ✅ Passed PASS: domain-facing context logic now sits in core/state/registration, while the only os.environ coupling is isolated in env_overlay.py; no adapter leakage into core was found.
Observability ✅ Passed PASS: The commit only edits docs/developers-guide.md; no runtime code, logs, metrics, traces, or alerts changed, so observability requirements are not applicable.
Security And Privacy ✅ Passed Treat this as safe: it is a package split only, with no new secrets, credentials, auth gaps, or unsafe sinks; the argv note warns about existing logging behaviour.
Performance And Resource Use ✅ Passed PASS: New loops are linear and bounded; env resolution copies os.environ only when needed, and worker threads are joined with a 10s cap.
Concurrency And State ✅ Passed Accept it: ContextVar-backed state is immutable, thread/task isolation and token restoration are tested, and stream worker threads have bounded joins/cancellation.
Architectural Complexity And Maintainability ✅ Passed PASS: The split isolates four real seams into sub-400-line modules, keeps init a thin façade, and uses _TokenRegistration only to deduplicate shared token-restoration logic.
Rust Compiler Lint Integrity ✅ Passed PASS: The Rust diff is comment-only; scans found no broad lint suppressions, touch helpers, or suspicious .clone() calls in the Rust sources.
📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #116

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-116-split-context-package

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

codescene-delta-analysis[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the issue-116-split-context-package branch from 52d04ee to b564a66 Compare June 11, 2026 21:31
codescene-delta-analysis[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the issue-116-split-context-package branch from b564a66 to fa2c4bd Compare June 11, 2026 22:22
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Jun 11, 2026

Copy link
Copy Markdown
Owner Author

CodeScene suppression request — Code Duplication in cuprum/context/registration.py

CodeScene flags: "The module contains 2 functions with similar structure: after, before" (advisory rule, new-file health 9.39).

Suggested suppression message for the CodeScene UI:

File: cuprum/context/registration.py
Symbols: before, after (and the sibling factory observe)
Reason: The similarity is superficial. These are one-line public factory functions whose bodies differ only in the hook-kind literal, but they exist precisely to give each hook kind a distinct, nominally typed entry point: before(hook: BeforeHook), after(hook: AfterHook), and observe(hook: ExecHook) accept different callable signatures and carry user-facing docstrings with kind-specific examples. Folding them into one parameterised factory would erase that typed API surface (callers would lose static checking of the hook signature against the registration kind). The behaviour they share — ContextVar token capture, idempotent detach(), and the context-manager protocol — is already centralised in the _TokenRegistration base introduced by this PR stack, so no logic is duplicated; only the thin, deliberately parallel API veneer remains.

If preferred, I can instead merge the three factories behind typing.overload signatures, but that trades three honest one-liners for an overload stack plus a runtime dispatcher, which seems strictly worse for legibility.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the issue-116-split-context-package branch from fa2c4bd to 1e8f75d Compare June 12, 2026 11:56
codescene-delta-analysis[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the issue-116-split-context-package branch from 1e8f75d to 685b933 Compare June 12, 2026 12:01
@lodyai
lodyai Bot force-pushed the issue-116-split-context-package branch from 685b933 to 182c9a3 Compare July 14, 2026 12:20
codescene-access[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the issue-116-split-context-package branch from 182c9a3 to 7b0685f Compare July 14, 2026 20:50
codescene-access[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the issue-116-split-context-package branch from 7b0685f to 73299d1 Compare July 15, 2026 13:07
codescene-access[bot]

This comment was marked as outdated.

@pandalump
pandalump marked this pull request as ready for review July 15, 2026 13:08
@coderabbitai coderabbitai Bot added the Issue label Jul 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cuprum/context/__init__.py`:
- Around line 75-101: Remove the six underscore-prefixed helper names from
__all__ in cuprum.context.__init__, while leaving their explicit module bindings
and implementations unchanged so direct imports and existing tests continue to
work.

In `@cuprum/context/registration.py`:
- Around line 119-126: Update Registration.detach so _detached is set only after
_reset_context successfully restores the captured token; keep the token intact
when restoration raises ValueError, allowing a retry in the originating context.
- Around line 191-207: Update the hook type branching in __init__ to explicitly
handle "before", "after", and "observe"; raise ValueError for any other value
instead of falling through to with_observe_hook. Preserve the existing hook
casting and context installation for the three supported discriminators.

In `@cuprum/unittests/test_token_registration_stateful.py`:
- Around line 109-113: Update
cuprum/unittests/test_token_registration_stateful.py at lines 109-113 to assert
current_context() is self._baseline after teardown drains _stack, preserving the
existing LIFO detach behavior. At lines 135-154, capture the caller context
before scoped(...) and assert current_context() is that context after scope
exit, while retaining the existing out-of-order detach assertions.
- Line 101: Update all four bare assertions in
cuprum/unittests/test_token_registration_stateful.py at lines 101, 107, 147, and
154 with the specified failure messages, covering first-detach restoration,
baseline restoration, inner-overlay discard, and captured-overlay restoration.
Preserve the existing assertion conditions and use assert-with-message syntax
throughout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fb8ca6a2-ced2-43c1-8c68-407095bf325e

📥 Commits

Reviewing files that changed from the base of the PR and between 6238fbd and 73299d1.

📒 Files selected for processing (10)
  • cuprum/context.py
  • cuprum/context/__init__.py
  • cuprum/context/core.py
  • cuprum/context/env_overlay.py
  • cuprum/context/registration.py
  • cuprum/context/state.py
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/test_token_registration_stateful.py
  • docs/developers-guide.md
  • typos.toml
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)
💤 Files with no reviewable changes (1)
  • cuprum/context.py

Comment thread cuprum/context/__init__.py
Comment thread cuprum/context/registration.py
Comment thread cuprum/context/registration.py
Comment thread cuprum/unittests/test_token_registration_stateful.py Outdated
Comment thread cuprum/unittests/test_token_registration_stateful.py
@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
cuprum/context/__init__.py (1)

75-101: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Strip the underscore-prefixed helpers out of __all__.

__all__ still lists _merge_after_hooks, _merge_before_hooks, _merge_observe_hooks, _narrow_allowlist, _resolve_narrowed_timeout, and _validate_timeout. The former cuprum.context.__all__ excluded these; keep the explicit module-level bindings (so cuprum.context._validate_timeout etc. still resolve for existing tests) but stop advertising them through the public surface / wildcard imports.

🐛 Proposed fix
 __all__ = [
     "AfterHook",
     "AllowRegistration",
     "BeforeHook",
     "CuprumContext",
     "EnvRegistration",
     "ExecHook",
     "ForbiddenProgramError",
     "HookRegistration",
     "ScopeConfig",
-    "_merge_after_hooks",
-    "_merge_before_hooks",
-    "_merge_observe_hooks",
-    "_narrow_allowlist",
-    "_resolve_narrowed_timeout",
-    "_validate_timeout",
     "after",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cuprum/context/__init__.py` around lines 75 - 101, Remove the
underscore-prefixed helper names from the __all__ list in the module while
leaving their module-level definitions and bindings unchanged, so direct access
such as cuprum.context._validate_timeout continues to work without exposing them
through wildcard imports.
cuprum/unittests/test_token_registration_stateful.py (2)

101-101: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Attach failure messages to every remaining bare assertion.

Lines 101, 107, 147, and 154 are still bare assert statements. Wire in the failure messages so a violated restoration invariant is identifiable from the test output alone.

Proposed assertion messages
-        assert after_first is prior
+        assert after_first is prior, "first detach must restore the prior context"
-            assert current_context() is self._baseline
+            assert current_context() is self._baseline, (
+                "an empty stack must restore the baseline context"
+            )
-        assert "CUPRUM_TEST_INNER" not in overlay
+        assert "CUPRUM_TEST_INNER" not in overlay, (
+            "outer detach must discard the inner overlay"
+        )
-        assert leaked.get("CUPRUM_TEST_OUTER") == "outer"
+        assert leaked.get("CUPRUM_TEST_OUTER") == "outer", (
+            "inner detach must restore its captured outer overlay"
+        )

As per path instructions, "Use assert …, "message" over bare asserts."

Also applies to: 147-147, 154-154, 107-107

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cuprum/unittests/test_token_registration_stateful.py` at line 101, Update the
bare assertions in the stateful token registration tests, including those around
after_first and the assertions at the other referenced locations, to use the
assert condition, message form. Provide each assertion with a clear message
identifying the violated restoration invariant.

Source: Path instructions


109-113: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert restoration after automatic cleanup and scope exit.

teardown() drains _stack but never asserts the state-machine baseline is actually restored afterwards, and test_out_of_order_detach_restores_outer_snapshot never checks that leaving the scoped(...) block restores the caller's own context. Either gap lets a final-step cleanup regression leak state silently.

Proposed test additions
 def teardown(self) -> None:
     """Detach any remaining handles in LIFO order."""
     while self._stack:
         handle, _prior = self._stack.pop()
         handle.detach()
+    assert current_context() is self._baseline, (
+        "teardown must restore the state machine baseline"
+    )
 def test_out_of_order_detach_restores_outer_snapshot() -> None:
     """..."""
+    caller_context = current_context()
     with scoped(ScopeConfig()):
         ...
         leaked = current_context().env_overlay or {}
         assert leaked.get("CUPRUM_TEST_OUTER") == "outer", (
             "inner detach must restore its captured outer overlay"
         )
+    assert current_context() is caller_context, (
+        "scope exit must restore the caller context"
+    )

As per coding guidelines, "New functionality and behavioral changes require substantive, non-vacuous tests" covering edge cases and functional boundaries.

Also applies to: 135-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cuprum/unittests/test_token_registration_stateful.py` around lines 109 - 113,
Update teardown() to assert that automatic cleanup restores the state-machine
baseline after draining _stack, using the existing baseline/state assertion
mechanism. Extend test_out_of_order_detach_restores_outer_snapshot to assert
that exiting the scoped(...) block restores the caller’s original context,
covering both automatic cleanup and scope-exit restoration.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cuprum/context/core.py`:
- Line 14: Update the timeout validation logic in the affected context
configuration flow to reject all non-finite values, including NaN and positive
or negative infinity, before the existing negative-value check proceeds. Use the
existing timeout validation symbols and raise ValueError consistently for these
invalid inputs.
- Around line 31-32: Define or reuse a package-level domain exception base, then
update ForbiddenProgramError to inherit from both that base and PermissionError.
Extend its initialization to accept and store the denied program and
restricted_state as attributes while preserving the existing descriptive
message.

In `@cuprum/unittests/test_token_registration_stateful.py`:
- Around line 103-107: Update stack tracking in the stateful test so each
registration stores both its prior context and installed context in self._stack.
Expand stack_depth_matches_context_nesting to assert current_context() matches
the baseline when empty and the top entry’s installed context when nested, and
adjust every self._stack.pop() unpacking site to handle the two stored values.

---

Duplicate comments:
In `@cuprum/context/__init__.py`:
- Around line 75-101: Remove the underscore-prefixed helper names from the
__all__ list in the module while leaving their module-level definitions and
bindings unchanged, so direct access such as cuprum.context._validate_timeout
continues to work without exposing them through wildcard imports.

In `@cuprum/unittests/test_token_registration_stateful.py`:
- Line 101: Update the bare assertions in the stateful token registration tests,
including those around after_first and the assertions at the other referenced
locations, to use the assert condition, message form. Provide each assertion
with a clear message identifying the violated restoration invariant.
- Around line 109-113: Update teardown() to assert that automatic cleanup
restores the state-machine baseline after draining _stack, using the existing
baseline/state assertion mechanism. Extend
test_out_of_order_detach_restores_outer_snapshot to assert that exiting the
scoped(...) block restores the caller’s original context, covering both
automatic cleanup and scope-exit restoration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6f447008-5fd5-4870-b930-c1fba8f36580

📥 Commits

Reviewing files that changed from the base of the PR and between 6238fbd and 73299d1.

📒 Files selected for processing (10)
  • cuprum/context.py
  • cuprum/context/__init__.py
  • cuprum/context/core.py
  • cuprum/context/env_overlay.py
  • cuprum/context/registration.py
  • cuprum/context/state.py
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/test_token_registration_stateful.py
  • docs/developers-guide.md
  • typos.toml
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)
💤 Files with no reviewable changes (1)
  • cuprum/context.py

Comment thread cuprum/context/core.py
Comment thread cuprum/context/core.py Outdated
Comment thread cuprum/unittests/test_token_registration_stateful.py Outdated
@lodyai
lodyai Bot force-pushed the issue-116-split-context-package branch from 73299d1 to ba6091a Compare July 15, 2026 22:15
codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for the following concern and provide an AI coding agent prompt for the fix:

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error The new stateful suite is weak: it only checks empty-stack restoration, so nested env registrations can overwrite outer overlays without failing. Add a nested env-registration test with disjoint keys and strengthen the stateful invariant to check the top stack context at every step.

@coderabbitai

This comment was marked as resolved.

leynos and others added 2 commits July 23, 2026 23:33
Address review feedback on the documentation:

- Add ADR-006 recording the decision to split `cuprum/context.py` into a
  `cuprum/context/` package, and index it from the developers guide.
- List `ContextError` alongside `CuprumContext`, `ScopeConfig`, and
  `ForbiddenProgramError` in the core.py package-layout bullet, documenting it
  as the package-level root of the domain exception hierarchy.
- Correct the retained debugging plan: the sampled commands come from the
  retained v2 plan and filter, not from an unchanged
  `ci_benchmark_ratchet_profile.py`; the current CI ratchet still raises
  `--runs` to 10 and interleaves each Python/Rust pair adjacently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback on the documentation:

- Note in the design doc that the CI benchmark ratchet also writes a skip
  report when the existing `main` baseline uses an incompatible (older)
  benchmark profile whose sampling protocol is not comparable, not only when no
  prior baseline exists — matching `ratchet_rust_performance.py` and the
  users-guide description.
- Use the `plaintext` fence language for the two hyperfine command blocks in the
  retained debugging plan, matching the plan's existing `plaintext` block;
  contents are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lodyai
lodyai Bot force-pushed the issue-116-split-context-package branch from dea79ea to b657190 Compare July 23, 2026 21:36
Collapse the double blank line left where the rebase merged the upstream
"Build and test worker controls" section against this branch's canonical
`_TokenRegistration` section, resolving an MD012 markdownlint violation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ 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 coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/users-guide.md`:
- Around line 1491-1493: Update the incompatible older benchmark-profile
baseline documentation in users-guide.md to state that, in addition to skipping
comparison, the system writes a skip report. Keep the explanation about
differing sampling protocols and worker timings unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fdd87ab1-343c-467d-8f1f-025e2837d062

📥 Commits

Reviewing files that changed from the base of the PR and between dacb76d and a28c9db.

📒 Files selected for processing (24)
  • benchmarks/_validation.py
  • benchmarks/benchmark_profile.py
  • benchmarks/ci_benchmark_ratchet_profile.py
  • cuprum/context.py
  • cuprum/context/__init__.py
  • cuprum/context/core.py
  • cuprum/context/env_overlay.py
  • cuprum/context/registration.py
  • cuprum/context/state.py
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/test_benchmark_ci_ratchet.py
  • cuprum/unittests/test_ci_benchmark_ratchet_profile.py
  • cuprum/unittests/test_context.py
  • cuprum/unittests/test_rust_splice.py
  • cuprum/unittests/test_rust_streams.py
  • cuprum/unittests/test_token_registration_stateful.py
  • docs/adr-006-context-package-split.md
  • docs/cuprum-design.md
  • docs/debugging/debugging-plan-2026-07-19T17-32-12Z.md
  • docs/developers-guide.md
  • docs/users-guide.md
  • rust/cuprum-rust/src/errors.rs
  • tests/behaviour/test_rust_streams_behaviour.py
  • tests/helpers/stream_pipes.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)
💤 Files with no reviewable changes (1)
  • cuprum/context.py

Comment thread docs/users-guide.md Outdated
State in the users guide that the benchmark ratchet writes a skip report (not
only skips comparison) when the saved `main` baseline uses an older, incompatible
benchmark profile shape — matching `ratchet_rust_performance.py`'s
`write_incompatible_profile_report` path and the design-doc wording. The
sampling-protocol/worker-timing explanation is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos enabled auto-merge (squash) July 24, 2026 00:18

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No quality gates enabled for this code.

@leynos
leynos merged commit 980e0cf into main Jul 24, 2026
20 checks passed
@leynos
leynos deleted the issue-116-split-context-package branch July 24, 2026 00:21
lodyai Bot pushed a commit that referenced this pull request Jul 24, 2026
The rebase onto main (#116/#157) introduced ADR-006 "Split cuprum/context.py
into a context package", colliding with this branch's ADR-006 "Subprocess
execution module boundaries". Main owns 006, so renumber the subprocess ADR to
007: rename the file and update every reference (contents.md, cuprum-design.md,
developers-guide.md, and the ADR title). Both ADRs are retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 25, 2026
The rebase onto main (#116/#157) introduced ADR-006 "Split cuprum/context.py
into a context package", colliding with this branch's ADR-006 "Subprocess
execution module boundaries". Main owns 006, so renumber the subprocess ADR to
007: rename the file and update every reference (contents.md, cuprum-design.md,
developers-guide.md, and the ADR title). Both ADRs are retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Jul 26, 2026
The rebase onto main (#116/#157) introduced ADR-006 "Split cuprum/context.py
into a context package", colliding with this branch's ADR-006 "Subprocess
execution module boundaries". Main owns 006, so renumber the subprocess ADR to
007: rename the file and update every reference (contents.md, cuprum-design.md,
developers-guide.md, and the ADR title). Both ADRs are retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
leynos added a commit that referenced this pull request Jul 26, 2026
* Split _subprocess_execution.py along its stated seams (#117)

`cuprum/_subprocess_execution.py` was 510 lines, carrying both a
`# pylint: disable=too-many-lines` pragma and a `# TODO: refactor into
smaller submodules` — the violation was suppressed rather than fixed. The
module bundled stdin writing, stream-consumer spawning, timeout handling,
and the top-level runner.

Split along the seams the TODO already named:

- `cuprum/_subprocess_stdin.py` — `_emit_stdin_error`, `_write_stdin`,
  `_spawn_stdin_writer`, with the `cuprum.stdin` logger now living in the
  module whose name matches it.
- `cuprum/_subprocess_timeout.py` — the timeout dataclasses and errors,
  `_handle_subprocess_timeout` / `_raise_timeout_expired` /
  `_handle_stream_timeout`, the exit-event helpers they share with the
  normal completion path, and a single `_require_timeout` guard that
  centralises the duplicated "TimeoutError without a configured timeout"
  check.
- `cuprum/_subprocess_execution.py` keeps the runner:
  `_execute_subprocess`, `_run_subprocess_with_streams`,
  `_spawn_subprocess`, and the stream-consumer wiring.

Remove the `too-many-lines` pragma and the TODO (all three modules are
now 103-274 lines), and drop the redundant `_resolve_timeout` re-export
from `__all__` — `cuprum/sh.py` imports it from its definition site
`cuprum._subprocess_context`. `test_observe` patches `_write_stdin` at
its new home. The wheel-build snapshot reflects the new file list.

* Refresh Oxford spelling policy after rebase

Regenerate the Typos configuration with the current policy inputs and use
`artefact` terminology in maintained prose so the spelling gate is
reproducible.

* Repair subprocess split after rebase (#117)

Restore the helper imports and spacing required by the split modules after
replaying the branch onto the current subprocess implementation.

* Address subprocess split review feedback (#117)

Use structural matching for timeout errors and document the accepted private
module boundaries. Correct the ExecPlan style and duplicate artefact section,
and record the non-reproducible Hypothesis health-check investigation.

* Refresh Rust availability UI snapshot

Capture the current compiler diagnostic for the intentionally non-const
availability export after rebasing onto main. Remove merge-created spacing
drift from the design document.

* Preserve timeouts during stream cleanup (#117)

Cancel stdin work before tolerant cleanup and retain timeout reporting when a
stream consumer fails. Cover that interleaving, update its wheel snapshot, and
apply the requested ADR and debugging-record documentation corrections.

* Pin Rust UI tests to CI toolchain (#117)

Align the workspace and lint job with Rust 1.85.0, then regenerate the
compile-fail expectation with that compiler. Remove a redundant test
closure return type so pinned Clippy remains warning-free.

* Cancel blocked stdin writers on subprocess timeout (#117)

Address review feedback on the subprocess-execution split.

- Manage the direct-mode stdin writer separately from
  `_wait_for_exit_code`'s consumers: on timeout or cancellation it is now
  cancelled and drained before the failure is translated or propagated, so a
  drain wedged on an unread pipe cannot delay completion. Extract the shared
  cancel-and-drain step into `_cancel_stdin_writer` and reuse it on the
  streamed cancellation path.
- Tighten the `_wait_for_exit_code` `consumers` type from `tuple[Task[Any],
  ...]` to `tuple[Task[None] | Task[str | None], ...]`. The timeout branch
  keeps gathering (not cancelling) its consumers, since after the stdin
  writer is managed separately the only remaining consumers are the stream
  readers whose partial output must survive on timeout.
- Add a regression test for a direct-mode timeout with a stdin payload that
  wedges the writer against a child that never reads stdin.
- Add a Hypothesis property test asserting `_handle_stream_timeout` upholds
  its cleanup contract (timeout preserved, stdin writer cancelled, consumer
  outcomes mapped) across arbitrary task orderings.
- Give the timeout-cleanup assertions descriptive failure messages.
- Debugging plan: sentence-case the section headings and add a
  both-hypotheses-supported branch to the termination criteria.

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

* Refine timeout consumers type and debugging-plan accuracy (#117)

Address the second round of review feedback.

- Narrow the `_wait_for_exit_code` `consumers` annotation to
  `tuple[asyncio.Task[str | None], ...]`. After the stdin writer is managed
  separately, no call site passes a `Task[None]`, so the union arm was dead.
  Default and call-site behaviour are unchanged.
- Debugging plan: state the observed `too_slow` health check without claiming
  the strategy caused it, and correct the H1 hypothesis to reflect that
  `_TAGS` is bounded and non-recursive (finite key set, bounded values,
  `max_size=3`). Replace `leta show _TAGS` (which needs an indexed leta
  workspace) with a repository-native `rg` command plus the exact focused
  `pytest` invocation and recorded seed, and add the missing comma between the
  two independent clauses in the recommended execution order.

The new `_handle_stream_timeout` property test was measured at ~0.13s for its
75 examples (the investigated slow test runs in ~0.19s); it lives in an
unrelated module and shares no strategies or fixtures with the `ctx_tags`
generation under investigation, so it does not add to that wall-clock cost and
was left unchanged.

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

* Bundle stream-timeout property inputs into a case dataclass (#117)

Resolve the CodeScene "Excess Number of Function Arguments" finding on
`test_handle_stream_timeout_upholds_invariants_across_orderings` by folding its
six generated `@given` arguments into a single frozen `_StreamTimeoutCase`
scenario built with `st.builds`. The per-field generation ranges, settings, and
every assertion (stdin cancellation, consumer draining, exception-to-None
mapping, and timeout preservation) are unchanged, so property-test coverage is
preserved. No production code changes and no CodeScene suppression.

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

* Share stdin-writer cleanup in stream timeout handler (#117)

Address the third round of review feedback.

- `_handle_stream_timeout` now delegates to the shared
  `_cancel_stdin_writer(stdin_task)` helper instead of duplicating the inline
  cancel-and-tolerant-gather logic, keeping the stdin-task lifecycle identical
  across the timeout and cancellation paths.
- Debugging plan: re-wrap the H1 claim so every prose line stays within 80
  columns, name the skipped `leta workspace add` indexing step in inline code
  (replacing the vague "workspace-indexing prerequisite" wording), and name the
  generated argument `ctx_tags` in the H1 ordering rationale so it explicitly
  identifies why H1 is the cheapest decisive check.

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

* Cancel pending stream consumers on subprocess timeout (#117)

Complete the `_wait_for_exit_code` timeout-cleanup finding for the stream path.

- Restore the `consumers` annotation to
  `tuple[asyncio.Task[None] | asyncio.Task[str | None], ...]` so the parameter
  accepts a blocking cleanup task as well as stdout/stderr readers.
- On `TimeoutError`, cancel any consumer still pending after
  `_terminate_process` before draining it, so a reader wedged on a pipe that
  never reached EOF cannot make timeout handling hang. Finished readers keep
  their captured output. Factor the guarded cancel loop into
  `_cancel_pending_consumers` and reuse it from both the timeout and
  cancellation branches, keeping them consistent (cancelling an already-done
  task is a no-op, so behaviour is unchanged).
- Add a focused regression test that drives `_wait_for_exit_code` into its
  timeout-cleanup branch with a blocking consumer and asserts the consumer is
  cancelled and drained while the original `TimeoutError` propagates.

The direct-mode stdin handling and its blocked-writer regression test are
unaffected.

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

* Cover the cancellation cleanup path in timeout tests (#117)

Address the fifth round of review feedback (test-only).

- Add `test_wait_for_exit_code_cancels_pending_consumers_on_cancellation`,
  which runs `_wait_for_exit_code` in a task, cancels it while a consumer is
  still pending, and asserts that `asyncio.CancelledError` propagates and the
  consumer is cancelled and drained. This covers the cancellation cleanup
  branch distinctly from the existing timeout test.
- Give the `_TimeoutWaitProcess.wait` assertion a diagnostic message spelling
  out the process-double invariant (terminate/kill must record `returncode`
  before `_exited` is set); wait/return behaviour is unchanged.
- Raise `ValueError` rather than `RuntimeError` from the property test's
  consumer helper when its outcome is "raise"; delay and return behaviour are
  unchanged.

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

* Cover streamed-run cancellation cleanup end-to-end (#117)

The `_wait_for_exit_code` cancellation branch was only exercised at the unit
level (a direct call with a fake process) and via direct-mode (capture=False)
cancellation tests; no test cancelled an in-flight streamed run.

Add `test_streamed_run_cancellation_cleans_up_task`, which cancels a running
`command.run(output=RunOutputOptions(capture=True))` mid-flight. Output capture
routes execution through `_run_subprocess_with_streams`, so this drives the
`CancelledError` cleanup with real stdout/stderr consumer tasks and asserts the
run tears down within a bounded time rather than deadlocking on a pending
reader. It complements the unit test
`test_wait_for_exit_code_cancels_pending_consumers_on_cancellation`, which
asserts the precise consumer cancel/drain state.

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

* Extract SafeCmd stdin lifecycle tests into their own module (#117)

Address the CodeScene "Lines of Code in a Single File" finding on
`test_safe_cmd_run.py` with a cohesive test-module extraction.

- Add `cuprum/unittests/test_safe_cmd_stdin.py` holding the stdin writer
  lifecycle regression group moved verbatim (assertions, commands, payload
  size, timeout values, and async/sync coverage unchanged):
  `test_stdin_input_with_timeout_escalation`,
  `test_direct_timeout_with_blocked_stdin_writer_does_not_hang`, and
  `test_stdin_input_cancellation_cleans_up_task`. The module carries only the
  imports and local helpers (`_execute_async`/`_execute_sync`, a local
  `python_builder` fixture) those tests need; `collections.abc` is imported
  under `TYPE_CHECKING` since it is annotation-only here.
- Remove those tests from `test_safe_cmd_run.py`, retaining
  `test_streamed_run_cancellation_cleans_up_task` (stream-consumer cleanup via
  captured execution) and the shared runtime helpers/imports still used there.
- Refresh the maturin wheel-build snapshot for the new module.

No production subprocess code changed and no CodeScene suppression added;
timeout, blocked-drain, cancellation, and execution-strategy coverage are
preserved. `test_safe_cmd_run.py` drops from 635 to 565 non-blank,
non-comment lines.

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

* Strengthen cancellation coverage and tighten subprocess typing (#117)

Address the seventh round of review feedback plus the failing Testing /
Concurrency checks.

Production:
- Narrow the `consumers` annotation on `_cancel_pending_consumers` and
  `_wait_for_exit_code` to `tuple[asyncio.Task[str | None], ...]`, matching what
  `_spawn_stream_consumers` actually returns; stdin remains handled separately
  by `_cancel_stdin_writer`.
- Merge the now-identical `except TimeoutError` and `except
  asyncio.CancelledError` branches of `_wait_for_exit_code` into a single
  `except (TimeoutError, asyncio.CancelledError)` clause; termination, consumer
  cancellation, tolerant gather, and bare re-raise are unchanged.
- Replace the bare `RuntimeError` raised by `_require_timeout` with a
  package-scoped `_SubprocessInvariantError(RuntimeError)`, so the impossible-
  state guard is distinguishable from unrelated runtime failures while staying
  catchable as a `RuntimeError` (message and chaining preserved).

Tests:
- Cancellation regressions now assert real propagation with
  `pytest.raises(asyncio.CancelledError)` and `task.cancelled()` instead of
  suppressing `CancelledError`, in both the stdin (`run()`) and streamed-run
  tests, so they fail if `run()` ever swallows cancellation.
- The stdin cancellation test now uses a child that never reads stdin plus a
  1 MiB payload, so it exercises cleanup of a writer genuinely blocked in
  `drain()`.
- The observe `stdin_error` test provokes a real EPIPE (child closes stdin +
  1 MiB payload) instead of monkeypatching `_write_stdin`.
- Type `_execute_async`/`_execute_sync` kwargs with a `_RunKwargs` TypedDict
  (no `Any`) and narrow `execution_strategy` to `Literal["async", "sync"]`;
  correct the module docstring (timeouts cover both strategies, cancellation
  only `run()`).
- `fail_consumer` and the two `blocking_consumer` doubles are retyped/retargeted
  (`ValueError`; `-> str | None`) to match the above.

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

* Restore import ordering after rebase (#117)

The rebase onto main (CQRS refactor #118) left `from pathlib import Path`
ahead of the plain `import` statements in test_safe_cmd_run.py after the
stdin-test-removal conflict resolution. Reorder it below the stdlib imports to
satisfy ruff's isort rule.

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

* Renumber subprocess ADR to 007 after rebase (#117)

The rebase onto main (#116/#157) introduced ADR-006 "Split cuprum/context.py
into a context package", colliding with this branch's ADR-006 "Subprocess
execution module boundaries". Main owns 006, so renumber the subprocess ADR to
007: rename the file and update every reference (contents.md, cuprum-design.md,
developers-guide.md, and the ADR title). Both ADRs are retained.

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

* Reconcile stream consumers on stdin-writer failure and de-flake tests (#117)

Address review feedback on the subprocess execution split:

- _run_subprocess_with_streams: if `await stdin_task` raises an unexpected
  exception (or a cancellation lands on that await), cancel and drain the
  stdout/stderr consumer tasks before the error propagates, mirroring the
  timeout and cancellation cleanup paths, so the consumers are not abandoned.
- Replace fixed-`asyncio.sleep` synchronisation hacks with deterministic
  readiness signals before cancellation:
  - test_streamed_run_cancellation_cleans_up_task waits for an observed stdout
    line via an observe hook.
  - test_stdin_input_cancellation_cleans_up_task waits for the stdin writer to
    begin (and wedge in drain) via a wrapped `_write_stdin`.
  - test_wait_for_exit_code_cancels_pending_consumers_on_cancellation awaits a
    new `wait_started` event on the `_TimeoutWaitProcess` double.
- test_observe_emits_stdin_error_event_when_process_closes_stdin_early sizes
  its stdin payload from the probed pipe capacity so drain() is guaranteed to
  block, replacing the assumed ~64 KiB pipe-buffer race.

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

* Drop unreproducible _TAGS timing step from H1 debug plan (#117)

The H1 plan's Table 2 and prediction claimed a bounded, representative
`_TAGS` timing measurement, but the documented tooling only replays the whole
property test (generation plus observation construction) with no isolated
sampling harness or acceptance threshold, and no such harness exists in the
repo. Remove the unsupported "time representative samples" step and reconcile
the prediction so the documented falsification is reproducible as written.

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

* Cover streamed-path consumer reconciliation on stdin-writer failure (#117)

Add a regression test for the streamed (capture) path: inject a failing stdin
writer so `await stdin_task` raises, and assert the stdout/stderr consumer
tasks are cancelled and drained rather than orphaned. The assertion runs inside
the running loop (before asyncio.run tears it down and cancels leftovers) so it
genuinely fails without the reconcile fix, closing the coverage gap flagged in
review for the fix in 10d28c2.

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

* Split test_safe_cmd_run.py into cohesive modules (#117)

test_safe_cmd_run.py was 828 lines, over the 400-line CodeScene "Lines of Code
in a Single File" limit. Move tests into focused modules by concern, without
changing any test behaviour, assertions, names, or async/sync coverage (the
collected node-id set is unchanged):

- test_safe_cmd_stdin.py (extended): stdin injection tests (text/bytes feeding,
  configured encoding, capture-disabled, early-close, and the forbidden-command
  vs stdin-encoding ordering contract) alongside the existing stdin writer
  lifecycle regressions. Adds a local `execution_strategy` fixture.
- test_safe_cmd_context.py (new): allowlist enforcement and before/after hook
  integration (FIFO/LIFO order, hook arguments, cancellation skipping after
  hooks).
- test_safe_cmd_streams.py (new): captured-stream (`capture=True`) cleanup on
  cancellation and stdin-writer failure, preserving the monkeypatch targets.
- test_safe_cmd_run.py (slimmed to 366 lines): general execution/output/env/
  cwd/timeout coverage plus the non-cooperative-kill escalation test.

Regenerate the native-wheel snapshot to package the two new test modules. No
production code changed; no CodeScene suppression added.

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

* Parametrize duplicated stdin-feeding tests (#117)

Resolve the CodeScene duplication finding in test_safe_cmd_stdin.py: the
near-identical test_input_text_feeds_stdin and test_input_bytes_feeds_raw_stdin
shared their whole control flow, differing only in the child script, the
StdinInput payload, and the expected stdout. Replace them with a single
parametrized test_input_feeds_stdin ("text" and "raw-bytes" cases), preserving
both payloads, scripts, expected output, and the run()/run_sync() coverage via
the existing execution_strategy fixture. test_input_text_uses_configured_encoding
stays a separate test.

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

* Align coverage job Rust toolchain with MSRV 1.85.0 (#117)

The coverage job pinned Rust 1.92.0 while the MSRV pin (rust-toolchain.toml),
the lint-test job, and the typecheck-test job all use 1.85.0. Build coverage
with the same toolchain as the rest of CI so the native extension and test
suite are exercised under the MSRV. Only the toolchain version changes; the
setup-rust action pin and all other CI configuration are unchanged.

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

* Address review feedback on subprocess stdin logging and test structure (#117)

- _subprocess_stdin._emit_stdin_error: record the failing exception's traceback
  via `_LOGGER.error(..., exc_info=exc)` and drop the now-redundant inline
  exception format argument, preserving the context fields and the
  `stdin_error` observation event. (Ruff LOG004 forbids `.exception()` in this
  standalone helper, so the explicit `exc_info=exc` form is used.) Update the
  `test_write_stdin_observes_error_events` log assertion to check `caplog.text`,
  since the exception detail now lives in the traceback rather than the message.
- Add `tests/helpers/execution.py` with the shared `_RunKwargs` TypedDict and
  `ExecuteFn` alias; import them in test_safe_cmd_stdin.py and
  test_safe_cmd_context.py instead of duplicating the definitions.
- test_safe_cmd_stdin.py: parameterise `test_input_feeds_stdin` with a frozen
  `_StdinFeedCase` dataclass (3 params); drop the unused `python_builder`/
  `execution_strategy` fixtures from `test_input_text_and_input_bytes_conflict`;
  switch the four string-parametrized tests to the `execution_strategy` fixture;
  add diagnostic messages to the bare assertions.
- test_safe_cmd_context.py: add diagnostic messages to every bare assertion.

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

* Tidy ADR index and subprocess module-boundary docs (#117)

- contents.md: add the missing ADR-005 and ADR-006 entries to the ADR index,
  between ADR-004 and ADR-007, in the established reference-link format.
- cuprum-design.md: move the "8.1.5 Subprocess execution module boundaries"
  subsection to after Figure 3's mermaid block so the figure caption and diagram
  stay together and 8.1.5 closes section 8.1.
- developers-guide.md: replace the duplicated lifecycle-boundary preface with a
  pointer to cuprum-design.md §8.1.5 and ADR-007, and move the maintainer
  placement guidance under its own "Subprocess execution module boundaries"
  heading.

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

* Make the stdin failure path diagnosable in traces (#117)

The `stdin_error` transition emitted by `_emit_stdin_error` was surfaced to
metrics (a counter) and logs, but the tracing hook grouped it with
`plan`/`stdin` and ignored it, so a stdin write/close failure left no trace, and
the event carried no stable operation/error-type fields (operation was dropped
entirely; error type was only embedded in the free-text `note`).

- events.py / _pipeline_types.py: add optional `operation` and `error_type`
  fields to the public `ExecEvent` and internal `_EventDetails`, wired through
  `_StageObservation.emit`.
- _subprocess_stdin._emit_stdin_error: populate `operation` (write/close) and
  `error_type` on the emitted `stdin_error` event. (Logging already records the
  exception traceback via `exc_info` and these fields via `extra`.)
- tracing_adapter: record `stdin_error` as a `cuprum.stdin_error` span event
  (correlated by exec_id) carrying operation/error_type/note, leaving the span
  open and unmarked since the failure is non-fatal. Consolidate the near-identical
  output and stdin-error span-event handlers into one `_record_span_event`,
  keeping the module under the 400-line limit.
- Tests: update the stdin_error observe expectations, extend the shared event
  factory to forward the new fields, and add a tracing regression test.

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

* Document the tracing adapter phase-dispatch and span-event patterns (#117)

The tracing adapter recently consolidated its two near-identical span-event
handlers into one `_record_span_event` and added a `stdin_error` phase, leaving
the developers' guide out of date. Update the "Tracing adapter span lifecycle"
section to standardise and document the patterns:

- Phase-dispatch policy: every ExecEvent phase falls into one of four
  categories (span lifecycle, span event, deliberately ignored, unhandled);
  new phases slot into this policy rather than an ad-hoc side path.
- One span-event recorder: stdout/stderr/stdin_error all route through
  `_record_span_event`, which copies whichever of line/operation/error_type/note
  are set onto a `cuprum.<phase>` event; new recording phases extend the shared
  field set instead of adding a bespoke method.
- Non-fatal events (stdin_error) are recorded but leave the span open and
  unmarked; only `exit` ends the span.
- `record_output` gates stdout/stderr but not stdin_error, so a stdin failure
  stays diagnosable when line output recording is off.

Docs-only; no code change.

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

---------

Co-authored-by: leynos <leynos@rohga>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: split cuprum/context.py (807 lines) into a context/ package

3 participants