Skip to content

Consolidate metrics/clock ports and tighten checkpoint metrics tests (#115) - #116

Merged
leynos merged 13 commits into
mainfrom
issue-115-metrics-port-consolidation-and-testing
May 30, 2026
Merged

Consolidate metrics/clock ports and tighten checkpoint metrics tests (#115)#116
leynos merged 13 commits into
mainfrom
issue-115-metrics-port-consolidation-and-testing

Conversation

@lodyai

@lodyai lodyai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds test_checkpoint_store_records_recovery_failure_metrics, a mock-driven test that covers the recovery_failure metrics path when an IntegrityError fires and the conflicting checkpoint cannot be recovered via get_by_idempotency_key.
  • Migrates the checkpoint metrics assertions in tests/canonical_storage/test_workflow_checkpoints.py to syrupy snapshots, using _StepClock to keep latency values deterministic and reusable from the helper _RecordingMetrics.as_snapshot(). This follows the pattern already established in tests/test_generation_orchestration_snapshots.py.
  • Consolidates the duplicate clock port abstractions by removing ChronoClockPort and _PerfCounterChronoClock from episodic/qa/chrono.py and reusing the canonical MonotonicClockPort and PerfCounterClock from episodic/observability.py. Re-exports MonotonicClockPort from episodic.qa for any consumers that previously imported ChronoClockPort through that surface.
  • Documents the canonical MetricsPort and MonotonicClockPort ports — and the narrower BoundedMetricsPort structural subtype — in episodic/observability.py, episodic/metrics_ports.py, the ChronoMetricsPort docstring, and docs/developers-guide.md.

Closes #115.

Test plan

  • pytest tests/canonical_storage/test_workflow_checkpoints.py (12 passed, 2 snapshots passed).
  • pytest tests/test_chrono.py tests/test_chrono_contracts.py tests/test_chrono_properties.py tests/test_chrono_langgraph.py tests/steps/test_chrono_steps.py tests/test_port_contracts.py (79 passed).
  • uv run ruff check / uv run ruff format --check on changed files.
  • uv tool run ty==0.0.32 check (whole repo).
  • uv run hecate check (architecture boundaries).
  • markdownlint-cli2 docs/developers-guide.md.

🤖 Generated with Claude Code

Summary by Sourcery

Consolidate observability port abstractions around canonical metrics and monotonic clock interfaces, and tighten workflow checkpoint metrics coverage and fixtures.

New Features:

  • Add snapshot-based assertions for workflow checkpoint metrics, including coverage of recovery-failure metrics when idempotency conflicts cannot be recovered.

Enhancements:

  • Refactor Chrono to reuse the shared MonotonicClockPort and PerfCounterClock instead of a bespoke clock port hierarchy, and re-export the canonical clock port from the QA package.
  • Extract shared workflow checkpoint test fixtures and deterministic observability test doubles into a dedicated support module for reuse.
  • Document the canonical MetricsPort and MonotonicClockPort abstractions, their relationship to BoundedMetricsPort, and recommended test adapters in the developer guide and related docstrings.

Tests:

  • Expand workflow checkpoint tests to cover recovery failure metrics paths using mocks and deterministically-timed clocks, and migrate metrics assertions to syrupy snapshots.

leynos and others added 3 commits May 29, 2026 16:39
Covers the case where IntegrityError fires and the conflicting
checkpoint cannot be recovered through get_by_idempotency_key. Uses
unittest.mock.AsyncMock to deterministically simulate the race so the
adapter's recovery_failures and latency metrics are exercised without
PostgreSQL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace explicit metrics list comparisons in both
test_checkpoint_store_records_checkpoint_metrics and
test_checkpoint_store_records_recovery_failure_metrics with syrupy
snapshot assertions, following the deterministic-fixture pattern from
test_generation_orchestration_snapshots.py. _RecordingMetrics gains an
as_snapshot helper that returns a stable dict-of-lists shape; the
deterministic _StepClock keeps latency values exact in the snapshot
without redaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove the parallel ChronoClockPort and _PerfCounterChronoClock from
episodic.qa.chrono and reuse MonotonicClockPort and PerfCounterClock
from episodic.observability instead. This eliminates the duplicate port
hierarchy and establishes a single canonical clock port for the
codebase.

Expose MonotonicClockPort from episodic.qa for external consumers that
previously imported ChronoClockPort, and update the developers' guide
and module docstrings to document MetricsPort and MonotonicClockPort as
the canonical observability ports, with BoundedMetricsPort retained as
a narrower structural subtype for feature-specific ports.

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

This comment was marked as outdated.

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bfbb4e10-f0a5-4e7d-8b25-b621b9823758

📥 Commits

Reviewing files that changed from the base of the PR and between e8d15af and ad768b6.

📒 Files selected for processing (1)
  • tests/canonical_storage/_workflow_checkpoint_support.py

Overview

This PR centralises observability abstractions, tightens checkpoint-metrics tests with deterministic test doubles and snapshot assertions, and implements a focused unit test for the checkpoint recovery-failure metrics path. It completes the follow-up work for issue #115 (post PR #98) to remove parallel clock/metrics hierarchies and establish canonical ports.

Key changes

Observability ports consolidation

  • Removed Chrono-specific clock abstractions: ChronoClockPort and _PerfCounterChronoClock from episodic/qa/chrono.py.
  • Canonicalised on MetricsPort and MonotonicClockPort in episodic/observability.py; PerfCounterClock is used as the default monotonic implementation.
  • episodic.qa now re-exports MonotonicClockPort; ChronoRuntimeEstimator now depends on MonotonicClockPort.
  • Clarified relationships between MetricsPort and the narrower BoundedMetricsPort in episodic/observability.py and episodic/metrics_ports.py.
  • Expanded docstrings and developer guide guidance to prefer the canonical ports for new instrumentation and to keep labels bounded.

Test infrastructure and metrics refactor

  • Added shared, deterministic test doubles and helpers in tests/canonical_storage/_workflow_checkpoint_support.py:
    • make_checkpoint(...) factory for stable WorkflowCheckpoint instances.
    • RecordingMetrics with as_snapshot() for deterministic snapshot-friendly metrics capture.
    • StepClock for deterministic monotonic time (1 ms step).
    • make_integrity_error() factory and a frozen VanishingConflictSession dataclass via make_vanishing_conflict_session() to mock the “conflicting checkpoint vanishes” recovery scenario and expose inner mocks for assertions.
  • Migrated checkpoint metrics assertions to syrupy snapshots and deterministic clock/metrics fakes.
  • Replaced local test helpers with the shared support module across the checkpoint tests.

New and updated tests

  • Added test_checkpoint_store_records_recovery_failure_metrics: mock-driven test that simulates a duplicate-key IntegrityError where the conflicting checkpoint cannot be recovered; asserts that the IntegrityError propagates and that the recovery_failure metric is recorded with expected labels (verified via snapshot).
  • Updated test_checkpoint_store_records_checkpoint_metrics to use syrupy snapshots and the new RecordingMetrics/StepClock.
  • Updated related concurrency/property tests to use make_checkpoint(...).

Documentation and minor changes

  • docs/developers-guide.md: added “Observability port abstractions” section documenting canonical ports and instrumentation guidance.
  • Chrono docs updated to reference the canonical MonotonicClockPort and to expand ChronoMetricsPort docstring.
  • Minor .gitignore addition: ignore .claude/.
  • Small docstring cleanup in support module.

Test plan / checks

  • Targeted pytest runs for checkpoint and chrono suites.
  • ruff/format and type checks.
  • Hecate architecture checks.
  • markdownlint on docs.

Files changed (high level)

  • Added: tests/canonical_storage/_workflow_checkpoint_support.py
  • Added: tests/canonical_storage/snapshots/test_workflow_checkpoints.ambr
  • Modified: tests/canonical_storage/test_workflow_checkpoints.py
  • Modified: episodic/observability.py, episodic/metrics_ports.py, episodic/qa/init.py, episodic/qa/chrono.py
  • Modified: docs/developers-guide.md
  • Modified: .gitignore
  • Minor docs change: docs/execplans/upgrade-python-to-3-14-adopt-concurrent-interpreters.md

Issues closed

  • Closes #115

Walkthrough

Consolidate canonical observability ports (MetricsPort, MonotonicClockPort), migrate Chrono to use them, update exports and developer docs, add deterministic test doubles (metrics, clock, checkpoint fixtures), and refactor checkpoint tests to use snapshot assertions with new snapshots.

Changes

Observability port consolidation and test instrumentation

Layer / File(s) Summary
Canonical observability ports
episodic/observability.py, episodic/metrics_ports.py
episodic/observability.py docstring expanded to describe MetricsPort (bounded-cardinality metrics with Mapping[str, str] labels) and MonotonicClockPort. episodic/metrics_ports.py clarifies BoundedMetricsPort as a narrower subtype retained for historical adapters.
Chrono port migration to canonical abstractions
episodic/qa/chrono.py
Remove local ChronoClockPort and _PerfCounterChronoClock. Import MonotonicClockPort and PerfCounterClock from episodic.observability. Update ChronoRuntimeEstimator.clock annotation and default factory. Expand ChronoMetricsPort docstring.
Port exports and developer documentation
episodic/qa/__init__.py, docs/developers-guide.md, docs/execplans/...
Import and export MonotonicClockPort in episodic.qa; remove ChronoClockPort from __all__. Add "Observability port abstractions" section in developers' guide; update Chrono package/maintainer guidance and minor execplan reflow.
Test support infrastructure
tests/canonical_storage/_workflow_checkpoint_support.py
Add make_checkpoint() for deterministic WorkflowCheckpoints, RecordingMetrics in-memory recorder with as_snapshot(), StepClock monotonic fake, make_integrity_error(), and make_vanishing_conflict_session() mock session.
Test refactoring with snapshot assertions
tests/canonical_storage/test_workflow_checkpoints.py, tests/canonical_storage/__snapshots__/test_workflow_checkpoints.ambr
Replace module-local helpers with shared imports. test_checkpoint_store_records_checkpoint_metrics uses snapshot and asserts metrics.as_snapshot(). Add test_checkpoint_store_records_recovery_failure_metrics. Add snapshot file with expected checkpoint and recovery-failure metric outputs.
Housekeeping
.gitignore
Add .claude/ to ignore rules.

Possibly related PRs

  • leynos/episodic#98: Overlaps with checkpoint storage tests and metrics/clock injection changes; relates to adding recovery-failure coverage and snapshot-based metrics assertions.
  • leynos/episodic#99: Touches observability/metrics abstractions and clocks; relates to consolidating canonical ports.

Poem

Align the clocks, unify the counts,
Chrono hands find common mounts.
Tests record in steady time,
Snapshots hold the measured rhyme.
🎵

🚥 Pre-merge checks | ✅ 20
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Consolidate metrics/clock ports and tighten checkpoint metrics tests (#115)' accurately reflects the primary changes: consolidation of observability ports and refactoring of checkpoint metrics tests, with issue #115 correctly referenced.
Description check ✅ Passed The description clearly details the four main objectives: adding recovery failure metrics test, migrating to snapshot assertions, consolidating clock port abstractions, and documenting canonical ports.
Linked Issues check ✅ Passed All three linked issue requirements are met: recovery_failure metrics path covered with new test, metrics assertions migrated to syrupy snapshots with deterministic clocks, and duplicate port abstractions consolidated by removing ChronoClockPort and reusing MonotonicClockPort.
Out of Scope Changes check ✅ Passed All changes align with the stated objectives and linked issue requirements; the minor line-wrapping change in the execplan document remains documentation-only and does not alter substance.
Docstring Coverage ✅ Passed Docstring coverage is 80.95% which is sufficient. The required threshold is 80.00%.
Testing (Overall) ✅ Passed Recovery_failure test verifies IntegrityError propagation with mocks. Metrics tests use deterministic clocks and snapshot assertions covering metric names, labels, latencies.
User-Facing Documentation ✅ Passed Changes affect internal developer module (episodic.qa) documented in developers-guide.md, not user-facing functionality requiring users-guide.md updates; developers-guide was updated appropriately.
Developer Documentation ✅ Passed New canonical observability ports documented in developers-guide.md with clear guidance. Module docstrings clarify port relationships. No multilingual or roadmap updates required.
Module-Level Documentation ✅ Passed All six Python modules modified in the PR carry module-level docstrings explaining purpose, utility, and relationships to other components as required.
Testing (Unit And Behavioural) ✅ Passed Recovery failure test exercises real store; test doubles implement public protocols; comprehensive coverage of persist, reuse, conflict and failure outcomes with proper error path assertions.
Testing (Property / Proof) ✅ Passed PR refactors existing code (consolidates ports, removes duplication) and adds tests for existing functionality; no new algorithmic invariants, state transitions, or proof assumptions are introduced.
Testing (Compile-Time / Ui) ✅ Passed Snapshots employ deterministic StepClock, explicit redaction documentation, focused assertions with bounded labels, semantic assertions around snapshots, and stable .ambr serialisation.
Unit Architecture ✅ Passed Consolidation maintains canonical port sources, explicit DI at all boundaries, visible fallibility handling, pure query paths, and single-purpose test doubles.
Domain Architecture ✅ Passed Metrics/clock ports properly confined to adapter boundaries with dependency injection. Domain code has zero observability dependencies. Consolidation eliminates duplicate hierarchies.
Observability ✅ Passed No operational behaviour changes introduced. Observability features (metrics, logging, clocks) pre-existed; PR refactors port consolidation and adds test coverage.
Security And Privacy ✅ Passed No secrets, credentials, or PII exposed. Test fixtures use deterministic fake data. Snapshots contain only operational metrics. Architectural consolidation of observability ports.
Performance And Resource Use ✅ Passed RecordingMetrics and StepClock use bounded per-test allocation; no algorithmic regressions, unbounded loops, repeated I/O, or unnecessary cloning detected.
Concurrency And State ✅ Passed Test doubles isolated to sequential tests; concurrent tests use immutable defaults (NoopMetrics, PerfCounterClock). Metrics recording is synchronous, preventing races between concurrent operations.
Architectural Complexity And Maintainability ✅ Passed Removes duplicate ChronoClockPort, consolidates to canonical MonotonicClockPort; test fixtures properly layered; clear reuse strategy documented; no circular dependencies.
Rust Compiler Lint Integrity ✅ Passed The custom check concerns Rust compiler lint suppressions; this PR modifies only Python, Markdown, and configuration files—no Rust code exists in the repository.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

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

View plan used: #115

✨ 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 issue-115-metrics-port-consolidation-and-testing

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

@sourcery-ai

sourcery-ai Bot commented May 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Consolidates observability port abstractions around the canonical MetricsPort/MonotonicClockPort, refactors workflow checkpoint tests to shared helpers and syrupy snapshots, and adds targeted metrics coverage for recovery-failure paths.

File-Level Changes

Change Details Files
Refactor workflow checkpoint tests to use shared helpers, deterministic clock, and syrupy metrics snapshots, and add coverage for recovery-failure metrics.
  • Replace inline WorkflowCheckpoint factory and metrics/clock fakes with shared make_checkpoint, RecordingMetrics, and StepClock helpers from a new support module.
  • Convert explicit metrics counter/latency assertions to a single snapshot assertion via RecordingMetrics.as_snapshot and a syrupy snapshot file.
  • Add a new mock-driven test that simulates an IntegrityError and unrecoverable idempotency conflict to assert recovery_failure metrics via snapshot.
  • Update existing tests to use make_checkpoint consistently for deterministic fixture construction.
tests/canonical_storage/test_workflow_checkpoints.py
tests/canonical_storage/_workflow_checkpoint_support.py
tests/canonical_storage/__snapshots__/test_workflow_checkpoints.ambr
Unify Chrono’s clock abstraction on the shared MonotonicClockPort/PerfCounterClock and simplify Chrono metrics port docs.
  • Remove ChronoClockPort protocol and its _PerfCounterChronoClock adapter in favor of importing MonotonicClockPort and PerfCounterClock from episodic.observability.
  • Update ChronoRuntimeEstimator to depend on MonotonicClockPort with PerfCounterClock as the default clock implementation.
  • Tighten ChronoMetricsPort’s docstring to describe it as a named narrowing of BoundedMetricsPort without new methods.
  • Re-export MonotonicClockPort from episodic.qa and stop exporting ChronoClockPort.
episodic/qa/chrono.py
episodic/qa/__init__.py
Document canonical observability ports and Chrono’s use of the shared clock port in the developer guide.
  • Add a new "Observability port abstractions" section describing MetricsPort, MonotonicClockPort, and their relationship to BoundedMetricsPort/BoundedValueMetricsPort, plus testing guidance.
  • Update the Chrono architecture and guidelines sections to reference MonotonicClockPort from episodic.observability instead of ChronoClockPort and to clarify metrics/latency wiring expectations.
docs/developers-guide.md
Housekeeping and configuration updates related to observability ports and metrics (no behavioral changes highlighted in the diff snippet).
  • Apply any necessary updates in observability and metrics_ports modules to document or align the canonical MetricsPort/MonotonicClockPort abstractions.
  • Keep repository hygiene by updating ignore/config files as needed for the new tests and docs.
episodic/metrics_ports.py
episodic/observability.py
.gitignore

Assessment against linked issues

Issue Objective Addressed Explanation
#115 Add a test that covers the recovery_failure metrics path in the workflow checkpoint store, by triggering an IntegrityError where the existing checkpoint cannot be recovered, asserting the IntegrityError is re-raised, and that recovery_failure metrics are recorded with the expected labels.
#115 Refactor test_checkpoint_store_records_checkpoint_metrics in tests/canonical_storage/test_workflow_checkpoints.py to assert structured metrics output using syrupy snapshots (with deterministic/explicit handling of non-deterministic fields), following the snapshot pattern used in test_generation_orchestration_snapshots.py.
#115 Eliminate duplicate clock/metrics port abstractions by consolidating on the canonical MetricsPort and MonotonicClockPort in episodic/observability.py, and reuse them from Chrono (episodic/qa/chrono.py / episodic/qa/__init__.py), updating documentation to describe this single source of truth.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Move the RecordingMetrics fake, the deterministic StepClock, and the
make_checkpoint fixture builder out of tests/canonical_storage/
test_workflow_checkpoints.py and into a new
_workflow_checkpoint_support module so the test file stays under the
project's 400-line module budget and remains focused on behaviour.

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

This comment was marked as outdated.

The .claude/ directory holds per-session runtime state created by the
local Claude Code harness (for example scheduled_tasks.lock). It is
not meant to be tracked in source control.

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

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review May 29, 2026 15:38

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e87ab32b8b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread episodic/qa/__init__.py
@coderabbitai coderabbitai Bot added the Issue label May 29, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 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 `@tests/canonical_storage/test_workflow_checkpoints.py`:
- Line 62: Rename the test function
test_checkpoint_store_get_returns_none_for_missingmake_checkpoint to remove the
accidental "make" token, e.g. rename it to
test_checkpoint_store_get_returns_none_for_missing_checkpoint so the name
matches the test intent and is searchable; update any references or imports that
call this function (if any) to the new name.
- Around line 173-193: Add an explicit assertion that the recovery lookup path
was exercised by asserting the session lookup was awaited/called when
save_or_reuse triggers the recovery branch: after the await
store.save_or_reuse(checkpoint) (inside the pytest.raises block or immediately
afterwards) assert that session.execute (the mock used for
get_by_idempotency_key lookup) was awaited/called (or that
SqlAlchemyWorkflowCheckpointStore.get_by_idempotency_key was invoked) to ensure
the test explicitly verifies the "get_by_idempotency_key" miss path rather than
inferring it from metrics.
🪄 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: 0e1c3770-46dc-4441-8b69-3a8fa5fd2306

📥 Commits

Reviewing files that changed from the base of the PR and between 44950c1 and e87ab32.

📒 Files selected for processing (9)
  • .gitignore
  • docs/developers-guide.md
  • episodic/metrics_ports.py
  • episodic/observability.py
  • episodic/qa/__init__.py
  • episodic/qa/chrono.py
  • tests/canonical_storage/__snapshots__/test_workflow_checkpoints.ambr
  • tests/canonical_storage/_workflow_checkpoint_support.py
  • tests/canonical_storage/test_workflow_checkpoints.py

Comment thread tests/canonical_storage/test_workflow_checkpoints.py Outdated
Comment thread tests/canonical_storage/test_workflow_checkpoints.py Outdated
sourcery-ai[bot]

This comment was marked as resolved.

@sourcery-ai sourcery-ai 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.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@leynos

leynos commented May 29, 2026

Copy link
Copy Markdown
Owner

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

The new test_checkpoint_store_records_recovery_failure_metrics uses fairly involved mocking; consider extracting a small helper/builder for the mocked AsyncSession/IntegrityError interaction so the test logic (expected behavior and assertions) is easier to scan.

@leynos

leynos commented May 29, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Please investigate whether the following issue is valid using codegraph exploration and research, identify a fix and provide an AI coding agent prompt for the fix if applicable:

The function name test_checkpoint_store_get_returns_none_for_missingmake_checkpoint looks like a typo from the make_checkpoint rename and should be corrected for clarity and consistency with the rest of the test names.

@coderabbitai

This comment was marked as resolved.

@leynos

leynos commented May 29, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Please investigate whether the following issue is valid using codegraph exploration and research, identify a fix and provide an AI coding agent prompt for the fix if applicable:

<location path="episodic/qa/chrono.py" line_range="53-56" />
<code_context>


 class ChronoMetricsPort(BoundedMetricsPort, typ.Protocol):
-    """Bounded-cardinality metrics sink for Chrono runtime estimation."""
-
-
-class ChronoClockPort(typ.Protocol):
-    """Monotonic clock used to measure Chrono orchestration latency."""
+    """Bounded-cardinality metrics sink for Chrono runtime estimation.

-    def monotonic_seconds(self) -> float:
-        """Return a monotonic timestamp in seconds."""
+    This protocol narrows :class:`episodic.metrics_ports.BoundedMetricsPort` to
+    name Chrono's metrics boundary; it does not introduce new methods. Adapters
+    that satisfy ``BoundedMetricsPort`` therefore satisfy this port as well.
</code_context>
<issue_to_address>
**question:** Double-check that inheriting from both `BoundedMetricsPort` and `Protocol` behaves as intended with your type checkers.

Since `ChronoMetricsPort` now derives from both `BoundedMetricsPort` and `typ.Protocol`, confirm how this is interpreted by your type checker(s) given how `BoundedMetricsPort` is defined. If you run into issues with protocol multiple inheritance or nominal vs structural typing, consider making `ChronoMetricsPort(Protocol)` only, and expressing the `BoundedMetricsPort` relationship structurally (e.g., via a `TypeAlias`) instead of via inheritance.
</issue_to_address>

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "test_checkpoint_store_get_returns_none" tests/canonical_storage/test_workflow_checkpoints.py

Length of output: 236



@leynos The issue is valid and confirmed. Line 62 of tests/canonical_storage/test_workflow_checkpoints.py reads:

async def test_checkpoint_store_get_returns_none_for_missingmake_checkpoint(

The _checkpoint suffix was accidentally replaced wholesale with make_checkpoint (no leading underscore) during the refactor, producing a nonsensical compound. The correct name is test_checkpoint_store_get_returns_none_for_missing_checkpoint.

Here is the AI coding agent prompt:

In `tests/canonical_storage/test_workflow_checkpoints.py`, line 62, rename the test function from

    test_checkpoint_store_get_returns_none_for_missingmake_checkpoint

to

    test_checkpoint_store_get_returns_none_for_missing_checkpoint

This is a pure identifier rename with no logic changes. Verify afterwards that no other file references the old name (e.g. via `rg "missingmake_checkpoint"`) and that `pytest -k test_checkpoint_store_get_returns_none_for_missing_checkpoint` passes.
  • Rename the typo'd function on line 62 of tests/canonical_storage/test_workflow_checkpoints.py

The requested changes have been implemented and a pull request has been created: View PR

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== episodic/metrics_ports.py ==="
cat episodic/metrics_ports.py
echo ""
echo "=== episodic/qa/chrono.py ==="
cat episodic/qa/chrono.py
echo ""
echo "=== episodic/observability.py (for MetricsPort) ==="
cat episodic/observability.py

Length of output: 16330



@leynos: The concern is not valid — the current code is correct.

Why the pattern is sound

BoundedMetricsPort is itself a Protocol:

# episodic/metrics_ports.py
class BoundedMetricsPort(typ.Protocol):
    ...

PEP 544 explicitly mandates that when you want a subclass to remain a protocol (i.e., structurally typed and matchable), you must include Protocol explicitly in its bases alongside the parent protocol. Without it, the subclass becomes a concrete class that nominally implements the parent protocol rather than extending it structurally. So:

class ChronoMetricsPort(BoundedMetricsPort, typ.Protocol):  # ✅ Correct — remains a Protocol
    ...

class ChronoMetricsPort(BoundedMetricsPort):  # ❌ Wrong — becomes a concrete class
    ...

Both mypy and pyright handle class Sub(ParentProto, Protocol) correctly. There is no nominal-vs-structural conflict because BoundedMetricsPort is entirely structural.

The codebase already applies this identical pattern in episodic/metrics_ports.py:

class BoundedValueMetricsPort(BoundedMetricsPort, typ.Protocol):
    ...

That precedent confirms the intent is consistent. No fix is required.

Move the `IntegrityError` construction and the vanishing-conflict
session mock into `make_integrity_error()` and
`make_vanishing_conflict_session()` factories in the support module.
This removes inline `mock.MagicMock`/`mock.AsyncMock` assembly from
the test body and keeps the test module focused on behavioural
assertions.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Rename `test_checkpoint_store_get_returns_none_for_missingmake_checkpoint`
to the intended `test_checkpoint_store_get_returns_none_for_missing_checkpoint`.
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== _workflow_checkpoint_support.py ==="
cat tests/canonical_storage/_workflow_checkpoint_support.py
echo ""
echo "=== test_workflow_checkpoints.py (relevant lines) ==="
rg -n "missingmake_checkpoint|missing_checkpoint|make_vanishing|make_integrity_error|recovery_failure_metrics" tests/canonical_storage/test_workflow_checkpoints.py
echo ""
echo "=== Check if helpers exist in support module ==="
rg -n "make_vanishing|make_integrity_error" tests/canonical_storage/_workflow_checkpoint_support.py

Length of output: 5366



@leynos: All three concerns are resolved.

Concern Status
Mocking extraction (make_integrity_error / make_vanishing_conflict_session) ✅ Both helpers are present in _workflow_checkpoint_support.py (lines 99 & 108) and test_workflow_checkpoints.py imports and uses them correctly.
Typo in function name ✅ Line 62 now reads test_checkpoint_store_get_returns_none_for_missing_checkpoint.
ChronoMetricsPort(BoundedMetricsPort, Protocol) inheritance ✅ No change required — confirmed valid per PEP 544 in the earlier reply.

No requirements were annulled; none violated en-GB-oxendict conventions or increased cyclomatic complexity.

@leynos

leynos commented May 29, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/canonical_storage/test_workflow_checkpoints.py (1)

147-166: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add explicit assertions that the recovery-lookup path was exercised.

The test relies solely on the metrics snapshot to verify the recovery-failure path. Once make_vanishing_conflict_session() is updated to return both the session and empty-result mocks (see comment on the support file), add assertions verifying session.execute was awaited and empty_result.scalar_one_or_none was called. This confirms the lookup path ran rather than inferring it from recorded metrics.

Proposed fix (after updating the helper)
-    store = SqlAlchemyWorkflowCheckpointStore(
-        typ.cast("AsyncSession", make_vanishing_conflict_session()),
+    conflict = make_vanishing_conflict_session()
+    store = SqlAlchemyWorkflowCheckpointStore(
+        typ.cast("AsyncSession", conflict.session),
         metrics=metrics,
         clock=StepClock(),
     )

     with pytest.raises(IntegrityError):
         await store.save_or_reuse(checkpoint)

+    conflict.session.execute.assert_awaited_once()
+    conflict.empty_result.scalar_one_or_none.assert_called_once_with()
     assert metrics.as_snapshot() == snapshot
🤖 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 `@tests/canonical_storage/test_workflow_checkpoints.py` around lines 147 - 166,
Update the test_checkpoint_store_records_recovery_failure_metrics test to
explicitly assert the recovery lookup path was executed: after modifying
make_vanishing_conflict_session to return (session, empty_result) mocks, call
the helper to obtain both, pass the session into
SqlAlchemyWorkflowCheckpointStore as before, then add assertions that
session.execute.await_count (or session.execute.assert_awaited() depending on
mock style) shows it was awaited and that empty_result.scalar_one_or_none was
called (e.g., empty_result.scalar_one_or_none.assert_called_once() or
await/await_count check). Keep the IntegrityError assertion and the final
metrics snapshot assertion; just insert these explicit calls referencing
make_vanishing_conflict_session, session.execute, and
empty_result.scalar_one_or_none to prove the lookup path ran.
🤖 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 `@tests/canonical_storage/_workflow_checkpoint_support.py`:
- Around line 108-127: Update make_vanishing_conflict_session to return a small
structured object (e.g., a `@dataclass` or simple namespace) that exposes both the
MagicMock session and the empty_result mock so tests can assert calls on them;
specifically keep the existing behavior of session.begin_nested, session.flush
(side_effect=make_integrity_error()), session.execute (returning empty_result),
but return something like {session: session, empty_result: empty_result} (or a
dataclass with attributes session and empty_result) and update any
tests/call-sites to reference the returned_object.session and
returned_object.empty_result when asserting call counts or call args.

---

Outside diff comments:
In `@tests/canonical_storage/test_workflow_checkpoints.py`:
- Around line 147-166: Update the
test_checkpoint_store_records_recovery_failure_metrics test to explicitly assert
the recovery lookup path was executed: after modifying
make_vanishing_conflict_session to return (session, empty_result) mocks, call
the helper to obtain both, pass the session into
SqlAlchemyWorkflowCheckpointStore as before, then add assertions that
session.execute.await_count (or session.execute.assert_awaited() depending on
mock style) shows it was awaited and that empty_result.scalar_one_or_none was
called (e.g., empty_result.scalar_one_or_none.assert_called_once() or
await/await_count check). Keep the IntegrityError assertion and the final
metrics snapshot assertion; just insert these explicit calls referencing
make_vanishing_conflict_session, session.execute, and
empty_result.scalar_one_or_none to prove the lookup path ran.
🪄 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: feb28a15-8197-48cd-8557-2ea262b5255e

📥 Commits

Reviewing files that changed from the base of the PR and between e87ab32 and 8a04b68.

📒 Files selected for processing (3)
  • docs/execplans/upgrade-python-to-3-14-adopt-concurrent-interpreters.md
  • tests/canonical_storage/_workflow_checkpoint_support.py
  • tests/canonical_storage/test_workflow_checkpoints.py

Comment thread tests/canonical_storage/_workflow_checkpoint_support.py Outdated
…ssion

Return a `types.SimpleNamespace` instead of the raw session mock so
callers can assert that the recovery lookup path was exercised
(``session.execute`` awaited and ``empty_result.scalar_one_or_none``
called).
codescene-delta-analysis[bot]

This comment was marked as outdated.

…overy test

Replace `types.SimpleNamespace` with a frozen `VanishingConflictSession`
dataclass so the return type is explicit. Switch `await_count` to
`assert_awaited_once()` and `assert_called_once()` to
`assert_called_once_with()` for clearer failure messages.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 29, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Have the following failed checks now been resolved?

If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures.

Do not treat warnings as optional or aspirational. Where a change is out of scope for this PR, propose a GitHub issue unless one exists already.

❌ Failed checks (2 errors, 5 warnings)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Approved review comment requested dataclass exposing session and empty_result for mock call verification. Not implemented—only session returned. Test relies solely on metrics snapshot assertions. Implement VanishingConflictSession dataclass return; add mock call assertions for recovery lookup execution paths.
Unit Architecture ❌ Error Review comment requesting VanishingConflictSession dataclass to expose inner mocks for explicit assertions was not implemented; test verifies only functional output, not contract boundaries. Modify make_vanishing_conflict_session() to return a dataclass with session and empty_result for explicit mock assertions verifying recovery-lookup path execution.
Testing (Unit And Behavioural) ⚠️ Warning Recovery test omits call-site assertions. Review requested exposing session and empty_result mocks to verify recovery-lookup path, but helper returns only session preventing mock call verification. Return VanishingConflictSession dataclass with session and empty_result mocks, then add test assertions for session.execute.assert_called_once() and empty_result.scalar_one_or_none.assert_called_once().
Testing (Compile-Time / Ui) ⚠️ Warning Snapshots are meaningful with stable serialisation, but make_vanishing_conflict_session() doesn't expose mocks as requested in review, preventing explicit call-site assertions. Refactor make_vanishing_conflict_session() to return a VanishingConflictSession dataclass with session and empty_result fields for test assertions.
Domain Architecture ⚠️ Warning Review comment requesting VanishingConflictSession dataclass was not addressed; function still returns mock.MagicMock, preventing assertion of recovery-lookup path invocations. Extract VanishingConflictSession as frozen dataclass with session and empty_result fields; return it so tests verify recovery-path method calls.
Concurrency And State ⚠️ Warning Review comment requesting mock exposure in make_vanishing_conflict_session() was not implemented; only session is returned, preventing test assertions on recovery-lookup execution. Return a VanishingConflictSession dataclass with both session and empty_result mocks, enabling assertions on the concurrent recovery path via session.execute() and scalar_one_or_none() calls.
Architectural Complexity And Maintainability ⚠️ Warning Review comment requesting make_vanishing_conflict_session return VanishingConflictSession dataclass with both session and empty_result was not implemented; still returns only session mock. Return a VanishingConflictSession dataclass containing both session and empty_result to enable explicit call-site assertions in tests.

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== _workflow_checkpoint_support.py ==="
cat tests/canonical_storage/_workflow_checkpoint_support.py
echo ""
echo "=== test_workflow_checkpoints.py (recovery_failure test) ==="
rg -n -A 40 "def test_checkpoint_store_records_recovery_failure" tests/canonical_storage/test_workflow_checkpoints.py
echo ""
echo "=== Check for VanishingConflictSession ==="
rg -n "VanishingConflictSession" tests/canonical_storage/_workflow_checkpoint_support.py tests/canonical_storage/test_workflow_checkpoints.py

Length of output: 7418



@leynos: All 7 failed checks are resolved in the current branch state.

Check Status Evidence
Testing (Overall) ✅ Resolved make_vanishing_conflict_session() returns VanishingConflictSession; test asserts conflict.session.execute.assert_awaited_once() and conflict.empty_result.scalar_one_or_none.assert_called_once_with()
Unit Architecture ✅ Resolved VanishingConflictSession is a frozen dataclass exposing both session and empty_result inner mocks
Testing (Unit And Behavioural) ✅ Resolved Explicit call-site assertions present on lines 167–168
Testing (Compile-Time / UI) ✅ Resolved Dataclass fields are accessible; snapshot assertion retained
Domain Architecture ✅ Resolved make_vanishing_conflict_session() return type is VanishingConflictSession, not mock.MagicMock
Concurrency And State ✅ Resolved Recovery-path method calls are asserted via assert_awaited_once() and assert_called_once_with()
Architectural Complexity And Maintainability ✅ Resolved Both mocks exposed via named fields on the returned dataclass

No further work is required for this PR.

@leynos

leynos commented May 29, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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.

1 similar comment
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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
Contributor

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 `@tests/canonical_storage/_workflow_checkpoint_support.py`:
- Line 109: Update the dataclass decorator to enable slots by changing
dataclasses.dataclass(frozen=True) to dataclasses.dataclass(frozen=True,
slots=True) for the dataclass defined at the decorator on the shown line (i.e.,
the class decorated with dataclasses.dataclass); ensure the signature is updated
everywhere that decorator is used for this class.
🪄 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: d77417db-14d7-4cd0-9a64-939720204d84

📥 Commits

Reviewing files that changed from the base of the PR and between 8a04b68 and e48f8d9.

📒 Files selected for processing (2)
  • tests/canonical_storage/_workflow_checkpoint_support.py
  • tests/canonical_storage/test_workflow_checkpoints.py

Comment thread tests/canonical_storage/_workflow_checkpoint_support.py Outdated
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 29, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/canonical_storage/_workflow_checkpoint_support.py (1)

1-6: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the module docstring to reference actual class and function names.

The docstring references `_RecordingMetrics`, `_StepClock`, and `_checkpoint` but the actual identifiers are RecordingMetrics (line 43), StepClock (line 88), and make_checkpoint (line 21). Correct the backticked references to match the code.

📝 Proposed fix
 """Shared test doubles for workflow checkpoint store tests.
 
-Hosts the deterministic ``_RecordingMetrics`` and ``_StepClock`` fakes used by
-``test_workflow_checkpoints.py`` and the ``_checkpoint`` fixture builder, so
+Hosts the deterministic ``RecordingMetrics`` and ``StepClock`` fakes used by
+``test_workflow_checkpoints.py`` and the ``make_checkpoint`` fixture builder, so
 the test module stays focused on behaviour rather than infrastructure.
 """
🤖 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 `@tests/canonical_storage/_workflow_checkpoint_support.py` at lines 3-4, update the module docstring to replace backticked references `_RecordingMetrics`, `_StepClock`, and `_checkpoint` with the actual identifiers used in the module: `RecordingMetrics`, `StepClock`, and `make_checkpoint` respectively; keep all other docstring text unchanged.
🤖 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 `@tests/canonical_storage/_workflow_checkpoint_support.py` around lines 1 - 6,
Update the module docstring so the backticked identifiers match the actual
symbols in the file: replace `_RecordingMetrics` with RecordingMetrics,
`_StepClock` with StepClock, and `_checkpoint` with make_checkpoint (remove the
extra leading underscores and use the exact names RecordingMetrics, StepClock,
make_checkpoint in backticks). Ensure only the docstring text is changed and the
rest of the file is untouched.
🤖 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.

Outside diff comments:
In `@tests/canonical_storage/_workflow_checkpoint_support.py`:
- Around line 1-6: Update the module docstring so the backticked identifiers
match the actual symbols in the file: replace `_RecordingMetrics` with
RecordingMetrics, `_StepClock` with StepClock, and `_checkpoint` with
make_checkpoint (remove the extra leading underscores and use the exact names
RecordingMetrics, StepClock, make_checkpoint in backticks). Ensure only the
docstring text is changed and the rest of the file is untouched.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e34c3c99-76df-4b29-988d-082c885dabfc

📥 Commits

Reviewing files that changed from the base of the PR and between e48f8d9 and e8d15af.

📒 Files selected for processing (1)
  • tests/canonical_storage/_workflow_checkpoint_support.py

@leynos
leynos merged commit 7e4631d into main May 30, 2026
4 checks passed
@leynos
leynos deleted the issue-115-metrics-port-consolidation-and-testing branch May 30, 2026 00:15
lodyai Bot pushed a commit that referenced this pull request May 30, 2026
The rebase onto `origin/main` brought in main's commit `7e4631d`
("Consolidate metrics/clock ports and tighten checkpoint metrics
tests (#115) (#116)"), which independently added `.claude/` near the
top of `.gitignore`. The earlier branch-local commit
`a59c343` ("Ignore `.claude/` Claude Code harness state") had added the
same entry at the bottom of the file. Drop the now-redundant trailing
entry; main's placement is kept because it groups with the other
`.cache`-style entries.
leynos added a commit that referenced this pull request May 31, 2026
* Plan REST surface hardening for 4.1.2

Draft the pre-implementation ExecPlan for roadmap item `4.1.2` at
`docs/execplans/4-1-2-finalize-rest-surfaces.md`. The plan covers eight
milestones that finalize REST surfaces for previously implemented `/v1`
canonical resources: a unified `{code, message, details}` error envelope
served through a single Falcon `add_error_handler`, the
`{items, limit, offset, total}` pagination envelope across every list
endpoint, consistent filter parameter parsing, and an
inbound-adapter-local `AuthorizationPort` scaffold with a permit-all
default. Full Role-Based Access Control (RBAC) and tenancy isolation
remain roadmap item `5.1`.

Decisions are recorded in the plan's `Decision log`: additive `count_*`
Protocol methods (so existing list signatures stay backwards-compatible),
Pattern B (centralised Falcon error handler) plus per-family
classification helpers, scaffold port placed at
`episodic/api/authorization.py`, resolved-bindings paginated at the API
layer, and routine readiness `503` responses exempt from the envelope
rewrite. References to `docs/episodic-tui-api-design.md`,
`docs/episodic-podcast-generation-system-design.md`, and ADRs 002, 009,
and 014 anchor the constraints. Three Wyvern research subagents
contributed the API surface inventory, the test coverage map, and the
hexagonal/architecture rails check that underpin the plan.

Implementation is gated on user approval.

* Ignore `.claude/` Claude Code harness state

The Claude Code harness writes a per-session lock file at
`.claude/scheduled_tasks.lock` (sessionId, pid, procStart, acquiredAt).
This is local tooling state, not source, and mirrors the existing
treatment of `.agents/mcp/context_pack/packs/.repo.lock` already
ignored at line 10. Drop the duplicated `.agents/...` entry while
adding `.claude/`.

* Refresh ExecPlan references for Hecate migration

Update `docs/execplans/4-1-2-finalize-rest-surfaces.md` after the rebase
onto `origin/main` picked up commit `3403ace` ("Adopt Hecate for
architecture checks (#107)"), which removed the repo-local
`episodic.architecture` module and moved policy enforcement into
`[tool.hecate]` in `pyproject.toml:437-496`.

- Replace `episodic/architecture/policy.py:<line>` citations with
  `pyproject.toml:<line>` citations covering the same group-prefix and
  allowed-import rules.
- Replace the `python -m episodic.architecture` recovery instruction
  with `uv run hecate check`, noting that diagnostic identifier
  `ARCH001` carries over.
- Append a revision note documenting the rebase and confirming the
  policy semantics (group prefixes, allowed imports) are
  byte-equivalent to the previous Python-module implementation, so no
  work-plan milestone change was required.

* Add REST error envelope handling

Introduce a Falcon ASGI error handler that serialises canonical API
HTTP errors as the documented `{code, message, details}` envelope.
Attach validation details in shared request parsers and preserve
profile/template optimistic-lock context for stale updates.

Update route-versioning, binding-resolution, profile/template, and
reference-document tests to assert the new envelope. Document the
contract in the developer guide and record Milestone 1 progress in the
active execplan.

* Add reference list totals

Add repository count methods and paged service wrappers for reusable
reference documents, document revisions, and bindings. Return `total` in
all three existing reference-domain list envelopes without changing the
legacy list service return types.

Extend reference API tests to assert totals for single-item helpers and
multi-page list responses.

* Add pagination to profile lists

Add paged profile/template repository and service reads so list
endpoints return `{items, limit, offset, total}`. Apply the same
envelope to resolved bindings and cover ordering in API tests.

* Add pagination to history endpoints

Thread paged history reads through the profile/template repository,
service, and shared Falcon history resource so history endpoints return
the standard pagination envelope and validate limit/offset inputs.

* Normalize REST filter parsing

Add shared optional UUID and enum query parsers, then use them
across list resources so invalid filters fail before service dispatch
with field-level validation error details.

* Add REST authorization scaffold

Introduce an async authorization port, permit-all adapter, and Falcon
middleware for /v1 requests. Cover unauthorized, forbidden, bypass,
and adapter-failure envelopes while documenting the scaffold.

* Align REST surface documentation

Document the standardized REST pagination and error envelopes,
filter parsing behaviour, and permit-all authorization scaffold across
the user guide, developer guide, and system design.

* Align lifespan test with pagination envelope

Update the canonical route smoke test to assert the final REST list
contract after the 4.1.2 pagination work.

Record the final gate findings in the ExecPlan, including the stale
assertion fix and the pre-existing guest-bios property failure that
remains outside this change.

* Mark REST surface roadmap item complete

Close out the 4.1.2 ExecPlan with final gate evidence, CodeRabbit
review results, and the known out-of-scope guest-bios property failure.

Mark roadmap item 4.1.2 complete after the final review returned zero
findings.

* Normalize ExecPlan Oxford spellings

Apply en-GB Oxford `-ize` spellings consistently in the 4.1.2
ExecPlan after review flagged mixed suffix forms.

* Normalize remaining ExecPlan spellings to Oxford -ize

The previous spelling-normalization pass missed four `-ise` variants in
`docs/execplans/4-1-2-finalize-rest-surfaces.md`. Switch each to its
Oxford `-ize` form per `en-gb-oxendict`:

- `initialisation` → `initialization`
- `organisation` → `organization`
- `standardised` → `standardized`
- `serialises` → `serializes`

Greek-origin `-lyse` words (`analyse`, `paralyse`) are not used in the
file and so remain unchanged; `surprise` (and its plural form
`surprises` used in the section heading) is preserved because it is
Old-French-derived and not subject to the `-ize` rule.

* Dedupe `.claude/` gitignore entry after rebase

The rebase onto `origin/main` brought in main's commit `7e4631d`
("Consolidate metrics/clock ports and tighten checkpoint metrics
tests (#115) (#116)"), which independently added `.claude/` near the
top of `.gitignore`. The earlier branch-local commit
`a59c343` ("Ignore `.claude/` Claude Code harness state") had added the
same entry at the bottom of the file. Drop the now-redundant trailing
entry; main's placement is kept because it groups with the other
`.cache`-style entries.

* Remove stray blank line in users guide

`make markdownlint` failed with MD012 on `docs/users-guide.md:200`
after the rebase onto `origin/main` left a stray double blank line
between the closing reference-documents bullet and the
`### REST API reference` heading. Drop the extra blank.

* Address reviewer findings on REST surface hardening

Apply the still-valid inline review comments:

- `docs/developers-guide.md`: switch `serialised` to `serialized` to
  match the en-GB Oxford `-ize` convention used elsewhere in the file.
- `episodic/api/authorization.py`: replace the `typ.assert_never` arm of
  the `AuthorizationDecision` match with an explicit logged
  `service_unavailable` 503 envelope so an unexpected enum value cannot
  escape as an unstructured 500.
- `episodic/api/errors.py`: add a `# parsed as tuple in Python 3`
  comment beside the `except IndexError, ValueError:` clause to flag
  the construct's modern semantics (the project's `ruff format`
  normalises this back to the bare form, so parenthesisation is not an
  option here).
- `episodic/canonical/profile_templates/services/_generic.py`: add a
  short justification to the `# noqa: PLR0913` on
  `list_history_paged`, matching the sibling
  `list_entities_with_revisions_paged` directly below.
- `episodic/canonical/storage/repositories.py`: append the record `id`
  as a deterministic tie-breaker to `created_at` in both
  `SqlAlchemySeriesProfileRepository.list` and
  `SqlAlchemyEpisodeTemplateRepository.list` so paginated lists do not
  drift when timestamps tie.
- `tests/api_fixtures.py`: introduce `_assert_total_at_least` and use
  it in both list-assertion helpers so the pagination envelope's
  `total` is verified as `>= len(items)` rather than `== len(items)`,
  matching the contract where `total` is the full filtered count and
  `items` is a page slice.
- `tests/test_api_authorization.py`: collapse the three
  decision-specific tests into one `pytest.mark.parametrize` over
  `(adapter_factory, expected_status, expected_payload)`. The
  health-bypass test stays separate because it asserts a different
  property.
- `tests/test_binding_resolution_api.py` and
  `tests/test_reference_document_roundtrip.py`: replace bare asserts
  on pagination envelopes with message-bearing forms that include the
  actual/expected values and the offending payload for debuggability.

Skip the four outside-diff refactor suggestions
(`handle_update_entity` split, `parse_pagination` helper extraction,
`_get_repos_for_kind` per-kind factories, `test_api_error_envelope.py`
parametrisation). They expand scope without fixing a bug, and the
user's "keep changes minimal" directive overrides; each remains a
reasonable follow-up if the project later wants the structural change.

* Revert unrelated mdformat reflow in docs

Restore four paragraphs in `docs/developers-guide.md`, two in
`docs/users-guide.md`, and one in
`docs/episodic-podcast-generation-system-design.md` to their
`origin/main` form. None of these paragraphs carry intentional `4.1.2`
content; they were touched only by a wider `make fmt` run that rewrapped
line breaks before/after parenthesised continuations or added stray
leading spaces. The four substantive doc additions for `4.1.2` (the
REST error contract, pagination/filter contract, and authorization
scaffold sections in the developer guide; the `/v1` REST API reference
section in the user guide; the `/v1` route prefix update and pagination
envelope update in the system-design reference REST API specification;
and the roadmap status flip) remain in place.

After this revert, `docs/users-guide.md` is purely additive against
`origin/main`, and the doc-diff stats shrink accordingly.

* Resolve CodeScene findings on REST surface refactor

Address five CodeScene findings raised on the 4.1.2 work without
introducing new lint suppressions. Each finding is fixed by reshaping
the code, not by silencing the rule.

- `handle_http_error` (5 args, max 4): replace
  `app.add_error_handler(falcon.HTTPError, handle_http_error)` with
  `app.set_error_serializer(serialize_http_error)`. The serializer
  hook has a 3-arg `(req, resp, exc)` signature, the function is
  synchronous (Falcon ASGI accepts both sync and async serializers,
  and the body performs no I/O), and the existing
  `noqa: PLR0913, PLR0917, RUF029` suppressions and pylint disables go
  away.
- `handle_update_entity` (70 lines, threshold 70): extract
  `_require_payload_fields(payload, required_fields)` and
  `_raise_mapped_update_error(exc, *, entity_id, payload)` so the
  public function is orchestration-only (~15 lines). The new helpers
  preserve the optimistic-lock context that
  `map_profile_template_error` needs.
- `_pagination_type_error_field` (Bumpy Road, two nested
  `try/except`): remove the helper. Replace its callers with a single
  `_parse_int_query_param(raw_value, *, name, default)` that returns
  the default when the parameter is absent and raises
  `validation_error` with the per-field name on parse failure. The
  resulting `parse_pagination` reads top-to-bottom with no nesting.
- `_assert_error_envelope` (5 args): introduce a frozen `_Expected`
  dataclass and pass it positionally. The helper becomes
  `(response, expected)` and all seven call sites supply the same
  `_Expected(...)` value.
- `list_entities_with_revisions_paged` and `list_history_paged`
  (5 args each): introduce a shared `Pagination` value object at
  `episodic/canonical/pagination.py` (added to the `domain_ports`
  Hecate group) carrying `(limit, offset)`. Both services now accept
  `page: Pagination`, and `parse_pagination` returns a `Pagination`
  instance so API resources pass it through unchanged. `noqa: PLR0913`
  on both services is removed.

A downstream effect of the `Pagination` value object: `handle_get_history`
went from 7 positional args to 6, then to 2 by bundling the per-call
inputs into a new `HistoryRequest` dataclass (entity id, id field name,
service, serializer, page). Its `noqa: PLR0913, PLR0917` suppression is
removed.

Tests updated:

- `tests/test_api_error_envelope.py`: all seven calls now pass
  `_Expected(...)` rather than keyword arguments.
- `tests/test_reference_document_validation.py`: pagination-type
  parametrize cases now expect the field-specific messages
  ("limit must be an integer.", "offset must be an integer.") that
  the new `_parse_int_query_param` produces.

Gates: `make check-fmt`, `make lint` (incl. `hecate check`, ruff,
pylint 10.00/10), `make typecheck`, `make markdownlint`, `make test`
(782 passed, 3 skipped).

* Address second reviewer pass on REST surface refactor

Inline findings:

- `episodic/api/handlers.py`: `_raise_mapped_update_error` no longer
  accepts a raw `payload`. It now takes the parsed
  `expected_revision: int | None` from `update_request.expected_revision`,
  so the `revision_conflict` envelope's `details.expected_revision` is
  always a typed integer and not a raw payload value.
- `episodic/canonical/reference_documents/bindings.py`,
  `documents.py`, `revisions.py`: the non-paged service functions
  (`list_reference_bindings`, `list_reference_documents`,
  `list_reference_document_revisions`) no longer call the `*_paged`
  variant and discard the total. Each now performs its own list-only
  repository call, sharing validation/lookup with the paged variant
  through a small private helper (`_prepare_*_list_query`). This
  removes one unnecessary `count_for_*` round-trip per call from any
  caller that does not need the total.
- `tests/test_api_error_envelope.py`:
  - the pagination test now sends a valid all-zeros UUID for
    `target_id` so the assertion focuses purely on the `limit` range
    check rather than relying on the (correct but coincidental)
    validation order;
  - `_assert_error_envelope` now asserts `payload["details"]` is a
    dict before casting, with an explicit failure message that names
    the actual type.
- `tests/test_profile_template_pagination_api.py`: replaced the
  cross-module private-helper imports of `_create_profile` /
  `_create_template` with a `canonical_api_creators` pytest fixture
  (defined in `tests/fixtures/api.py`) that bundles both creators in
  a frozen `CanonicalApiCreators` dataclass. Bundling them keeps the
  test signature within Pylint's default `max-args=4`.

Duplicate finding:

- `episodic/canonical/profile_templates/services/_generic.py`:
  extracted the per-kind branches of `_get_repos_for_kind` into
  `_build_series_profile_dispatch(uow)` and
  `_build_episode_template_dispatch(uow)`. The dispatcher is now a
  thin five-line match; each branch helper owns its casts, nested
  async wrappers, and dispatch-table construction.

Skipped outside-diff finding (with reason):

- `episodic/api/resources/resolved_bindings.py` push-pagination into
  `resolve_bindings`. The current pre-load-then-slice approach is
  correct for the expected dataset size (a handful of reference
  bindings per series). Pushing pagination into `resolve_bindings`
  would require changing the domain service signature, adding new
  repository methods, and answering non-trivial questions about how
  to count and page the result of a multi-stage in-memory grouping
  algorithm. That belongs to a dedicated design pass once dataset
  growth makes it material, not to this commit.

Config:

- `pyproject.toml`: extend `[tool.ruff.lint.per-file-ignores]` so
  `tests/fixtures/*.py` shares the lenient ruleset already applied to
  `**/test_*.py` and `tests/steps/*.py`. These files are pytest
  plugins, functionally test code; the existing config simply did not
  cover them by glob.
- `tests/fixtures/llm.py`: drop now-redundant `noqa: PLR0913` from
  `_build_adapter` (covered by the new fixtures ignore).

Gates: `make check-fmt`, `make lint` (incl. `hecate check`, ruff,
pylint 10.00/10), `make typecheck`, `make markdownlint`, `make test`
(782 passed, 3 skipped).

* Extract page-total assertions from round-trip test

`test_reference_document_lists_report_total_across_pages` exceeded the
70-line CodeScene threshold for a single function because it interleaved
setup, request, and assertion code for three distinct list endpoints
(reference documents, revisions, bindings). Extract one helper per stage:

- `_assert_document_page_total(client, profile_id) -> list[str]`
  creates three reference documents and asserts the first page of
  `/v1/series-profiles/{profile_id}/reference-documents`.
- `_assert_revision_page_total(client, profile_id, document_id) -> list[str]`
  creates two revisions on the given document and asserts the first page of
  `.../reference-documents/{document_id}/revisions`.
- `_assert_binding_page_total(client, revision_ids, template_id) -> None`
  binds each revision to the template and asserts the first page of
  `/v1/reference-bindings`.

The test function now reads top-to-bottom in 12 lines: build fixture →
document page → revision page → binding page. The sibling test
`test_reference_document_round_trip_and_binding_workflow` is untouched.
Behaviour is unchanged; both tests pass in ~5 s.

* Extract helpers from resolved-bindings round-trip test

`test_resolved_bindings_endpoint_returns_resolved_payloads` ran roughly
79 lines because it interleaved three concerns inside one test body:
creating a series-profile binding, creating an episode-template binding,
and asserting the pagination envelope plus item content of the
resolved-bindings response. Extract one helper per concern:

- `_create_series_profile_binding(client, profile_id, episode_id) -> str`
  creates the style-guide document, one revision, and the series-profile
  binding, returning the revision id.
- `_create_episode_template_binding(client, profile_id, template_id) -> str`
  creates the guest-profile document, one revision, and the
  episode-template binding, returning the revision id.
- `_assert_resolved_bindings_payload(payload, series_revision_id,
  template_revision_id)` asserts the limit/offset/total envelope and the
  per-item revision and document content.

The test now reads top-to-bottom in ~53 lines: build fixture, create
episode, create the two bindings via the new helpers, request the
resolved-bindings endpoint, and assert. Sibling tests in the file are
untouched. Behaviour is unchanged; all 8 tests in the file still pass in
~17 s.

* Parametrise canonical error-envelope tests

CodeScene flagged seven structurally identical test functions in
`tests/test_api_error_envelope.py`. Each ran the same two-step body —
`canonical_api_client.simulate_get(path, params=...)` followed by
`_assert_error_envelope(response, _Expected(...))` — and varied only in
the URL path, optional query params, and the `_Expected` envelope.

Collapse them into a single `test_error_envelope` driven by
`@pytest.mark.parametrize`. Each `pytest.param` carries the original
inputs and an explicit `id=` matching the prior test's intent
(`invalid_uuid_path_segment`, `invalid_pagination_bounds`,
`invalid_optional_uuid_filter`, `invalid_reference_document_kind_filter`,
`invalid_reference_binding_target_kind`,
`missing_required_query_parameter`, `unknown_identifier`), so failing
cases still report a descriptive identifier.

The `_Expected` dataclass and `_assert_error_envelope` helper are
untouched; only the import block grows by one line (`import pytest`).
All seven parametrised cases pass. No external module imported any of
the removed function names.

* Inline kind dispatch builders into `_get_repos_for_kind`

CodeScene flagged the two private helpers
`_build_series_profile_dispatch` and `_build_episode_template_dispatch`
as a code-duplication finding: they shared structure but diverged on
the entity kind. Inline each builder's body directly into the
corresponding match arm of `_get_repos_for_kind`, then delete both
helpers. The closure names (`_list_entities`, `_count_entities`,
`_list_history_paged`) are intentionally identical across the two arms;
only one arm runs at runtime, so the shared names cause no collision.

Two implementation notes:

- The episode-template arm now wraps `template_repo.count` in an async
  closure that threads `series_profile_id` through to
  `template_repo.count(series_profile_id)`. The previous helper passed
  the bound method directly, which worked because
  `template_repo.count` already accepts the same parameter, but the
  explicit closure mirrors the series-profile arm and matches the
  task's target shape.
- Per-arm history-repository casts use distinct names
  (`profile_history_repo`, `template_history_repo`) rather than a
  shared `history_repo`. Python's `match` does not introduce a fresh
  scope per arm, so a shared name would be unioned by static analysis
  and the per-arm closures could not resolve their kind-specific
  methods (`list_for_profile_paged` vs `list_for_template_paged`). The
  short comment at the top of the function records this for future
  readers.

`_get_repos_for_kind` carries a single targeted suppression,
`# noqa: C901`, with a justification tying the lift in cyclomatic
complexity to this design choice (extraction was just removed at the
reviewer's request). This is the rule's last-resort case: the design
and the linter conflict, and the design has been explicitly chosen.

Behaviour is unchanged. Full test suite: 782 passed, 3 skipped.
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.

Follow-up: three warnings from PR #98 checkpoint atomicity review

1 participant