Skip to content

refactor: replace _ErrorResponseSpec NamedTuple with TypedDict#554

Merged
Aureliolo merged 4 commits intomainfrom
refactor/error-responses-typeddict
Mar 18, 2026
Merged

refactor: replace _ErrorResponseSpec NamedTuple with TypedDict#554
Aureliolo merged 4 commits intomainfrom
refactor/error-responses-typeddict

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Convert _ErrorResponseSpec from NamedTuple to TypedDict so entries are plain dicts with key-only access, eliminating positional indexing
  • Update all field access from attribute style (spec.field) to bracket style (spec["field"])
  • Replace 5 pre-existing em-dash Unicode escapes (\u2014) with ASCII dashes (--) in error description strings

Closes #509

Review coverage

Pre-reviewed by 9 agents: docs-consistency, code-reviewer, python-reviewer, type-design-analyzer, pr-test-analyzer, conventions-enforcer, logging-audit, resilience-audit, issue-resolution-verifier. 2 findings addressed (em-dashes fixed, ReadOnly annotations evaluated and dropped as unnecessary for private module-level constant).

Test plan

  • All 100 existing unit tests pass unchanged (verified)
  • Full suite: 9409 passed, 0 failures (verified)
  • mypy strict: clean (verified)
  • ruff lint + format: clean (verified)
  • No behavior change -- identical OpenAPI schema output

🤖 Generated with Claude Code

Aureliolo and others added 2 commits March 18, 2026 18:52
Converts _ErrorResponseSpec from NamedTuple to TypedDict so entries
are plain dicts with key-only access, eliminating positional indexing.

Closes #509

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-reviewed by 9 agents, 2 findings addressed (em-dashes, ReadOnly
annotations evaluated and dropped as unnecessary for private constant).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 18, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 18, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8668d610-5c71-4a37-b3b1-7f3d1dcbcd00

📥 Commits

Reviewing files that changed from the base of the PR and between e3972eb and fb40127.

📒 Files selected for processing (1)
  • src/synthorg/api/openapi.py

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Standardized punctuation in API error messages and related descriptive text (em dashes replaced with ASCII double dashes).
    • Minor punctuation tweaks in internal comments and docstrings.
    • No changes to runtime behavior, public interfaces, or exported entities—API responses remain functionally identical.

Walkthrough

Replaced Unicode em dashes with ASCII double dashes and made small punctuation/text adjustments in error description strings and comments in src/synthorg/api/openapi.py. No public signatures, control flow, or functional behavior were changed.

Changes

Cohort / File(s) Summary
OpenAPI error text edits
src/synthorg/api/openapi.py
Replaced several error description strings' Unicode em dashes with ASCII double dashes; updated RFC 9457 prose and minor comment/docstring punctuation. No API surface, signatures, or logic changes.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~2 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 4

❌ Failed checks (2 warnings, 2 inconclusive)

Check name Status Explanation Resolution
Title check ⚠️ Warning Title claims TypedDict refactor, but PR actually reverts to NamedTuple per commit message and description. Update title to reflect actual changes: 'refactor: replace _ErrorResponseSpec NamedTuple, clean up em-dashes' or similar.
Description check ⚠️ Warning Description mentions TypedDict conversion and bracket-style access, but PR description and commit message reveal reversion to NamedTuple with attribute-style access. Correct description to accurately reflect NamedTuple usage and attribute-style field access, not TypedDict and bracket notation.
Linked Issues check ❓ Inconclusive PR partially addresses #509: NamedTuple (instead of TypedDict) replaces tuple-of-tuples, and noqa comment removal appears met, but bracket-style access claim is inconsistent with actual attribute-style implementation. Clarify whether NamedTuple satisfies issue #509's intent (named type + single parameter) and verify noqa removal from _build_reusable_response.
Out of Scope Changes check ❓ Inconclusive Em-dash replacement to ASCII dashes (5+ instances) is a secondary change beyond the core struct refactor requested in issue #509. Verify whether em-dash cleanup is in scope for #509 or should be split into a separate issue/PR for better change isolation.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ 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 refactor/error-responses-typeddict
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch refactor/error-responses-typeddict
📝 Coding Plan
  • Generate coding plan for human review comments

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

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refactors the internal representation of error response specifications by migrating from NamedTuple to TypedDict. This change enhances type safety and clarity by explicitly defining dictionary-like structures, allowing for more idiomatic key-based access to fields. Additionally, it standardizes the use of ASCII dashes in error messages for improved consistency and readability.

Highlights

  • Type Definition Change: The _ErrorResponseSpec class was converted from a NamedTuple to a TypedDict to allow for more flexible dictionary-like access and improved type hinting for dictionary structures.
  • Field Access Update: All instances of accessing fields on _ErrorResponseSpec objects were updated from attribute-style (e.g., spec.field) to bracket-style (e.g., spec["field"]).
  • Text Standardization: Five Unicode em-dash characters (\u2014) in error description strings were replaced with ASCII double-dashes (--) for consistency.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 18, 2026 18:23 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors _ErrorResponseSpec from a NamedTuple to a TypedDict to prevent positional indexing, and updates all access to use dictionary-style bracket notation. The changes also replace unicode em-dashes with ASCII dashes for consistency. The refactoring is correct and achieves its goal. However, I've noted a concern about the loss of runtime immutability for the _ERROR_RESPONSES constant, which now contains mutable dictionaries.



class _ErrorResponseSpec(NamedTuple):
class _ErrorResponseSpec(TypedDict):
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.

medium

Changing from NamedTuple to TypedDict makes the elements of _ERROR_RESPONSES mutable dicts. This removes the runtime immutability guarantee that NamedTuple provided, creating a risk of accidental modification of this constant data. Since this data structure is intended to be a constant, it should ideally be immutable to prevent future bugs.

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 18, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.49%. Comparing base (9b5655b) to head (fb40127).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #554   +/-   ##
=======================================
  Coverage   92.49%   92.49%           
=======================================
  Files         542      542           
  Lines       26655    26655           
  Branches     2544     2544           
=======================================
  Hits        24655    24655           
  Misses       1598     1598           
  Partials      402      402           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…shes

Revert _ErrorResponseSpec from TypedDict to NamedTuple to preserve
runtime immutability guarantees (TypedDict instances are mutable dicts).
Restore attribute-style access (spec.status vs spec["status"]).
Replace 3 remaining em-dash characters in comments/docstrings with
ASCII dashes.

Addresses review findings from 5 independent sources (conventions-enforcer,
python-reviewer, type-design-analyzer, gemini-code-assist, code-reviewer).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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)
src/synthorg/api/openapi.py (1)

373-401: 🛠️ Refactor suggestion | 🟠 Major

Return updated paths instead of mutating the argument.

This helper mutates the passed paths mapping in place and the new docstring now makes that part of its contract. Prefer returning an updated copy and assigning it at the call site so the helper stays reusable and aligned with the repo’s immutability rule.

♻️ Refactor sketch
 def _inject_operation_responses(
     paths: dict[str, Any],
     response_keys: list[str],
     status_for_key: dict[str, str],
-) -> None:
+) -> dict[str, Any]:
     """Inject error response refs into each operation in *paths*.
 
-    Mutates *paths* in place -- the caller is responsible for
-    passing a deep-copied schema.
+    Return a copy of *paths* with reusable error response refs injected.
     """
-    for path, path_item in paths.items():
+    updated_paths = copy.deepcopy(paths)
+    for path, path_item in updated_paths.items():
         for method, operation in path_item.items():
             if not isinstance(operation, dict) or "responses" not in operation:
                 continue
             op_responses = operation["responses"]
             for key in response_keys:
                 status_code = status_for_key[key]
                 if not _should_inject(key, path, method, operation):
                     continue
                 if status_code == "400":
                     # Only replace Litestar's auto-generated
                     # ValidationException 400; preserve custom 400s.
                     existing = op_responses.get("400")
                     if existing is None or _is_litestar_validation_400(
                         existing,
                     ):
                         op_responses["400"] = _response_ref(key)
                 elif status_code not in op_responses:
                     op_responses[status_code] = _response_ref(key)
+    return updated_paths
-    _inject_operation_responses(
-        result.get("paths", {}),
-        response_keys,
-        status_for_key,
-    )
+    if "paths" in result:
+        result["paths"] = _inject_operation_responses(
+            result["paths"],
+            response_keys,
+            status_for_key,
+        )
As per coding guidelines: "Immutability: create new objects, never mutate existing ones."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/openapi.py` around lines 373 - 401, The helper
_inject_operation_responses currently mutates the passed-in paths dict; change
it to return a new updated mapping instead: create a shallow/deep copy of paths
at the start (e.g., new_paths = copy.deepcopy(paths)), operate on new_paths
(using the same logic that references _should_inject,
_is_litestar_validation_400, and _response_ref) and return new_paths at the end,
and update call sites to assign the result back instead of relying on in-place
mutation so the function is immutable and side-effect free.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/api/openapi.py`:
- Around line 349-353: The comment and code that exclude "Conflict" for DELETE
(in the dict construction with keys "BadRequest", "NotFound", "Conflict") makes
a protocol-level claim ("DELETE excluded -- idempotent; conflicts are a
create/update concern.") which is incorrect; update the inline comment next to
the "Conflict" entry (and/or surrounding docstring) to state the
project-specific invariant that justifies excluding 409 for method == "delete"
(or remove the exclusion if no such invariant exists). Specifically, locate the
dict where "Conflict": is_write and method != "delete" is set and either (a)
replace the current justification with a clear project-level explanation naming
the invariant/assumption that prevents DELETE from returning 409, or (b) if no
invariant applies, remove the method != "delete" clause so DELETE can map to
"Conflict" like other write methods.

---

Outside diff comments:
In `@src/synthorg/api/openapi.py`:
- Around line 373-401: The helper _inject_operation_responses currently mutates
the passed-in paths dict; change it to return a new updated mapping instead:
create a shallow/deep copy of paths at the start (e.g., new_paths =
copy.deepcopy(paths)), operate on new_paths (using the same logic that
references _should_inject, _is_litestar_validation_400, and _response_ref) and
return new_paths at the end, and update call sites to assign the result back
instead of relying on in-place mutation so the function is immutable and
side-effect free.
🪄 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

Run ID: 3b5cb8f8-4afe-428f-b359-16674e10ffbb

📥 Commits

Reviewing files that changed from the base of the PR and between 8c942a8 and e3972eb.

📒 Files selected for processing (1)
  • src/synthorg/api/openapi.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations
PEP 758 except syntax: use except A, B: (no parentheses) — ruff enforces this on Python 3.14
Line length: 88 characters (ruff)

Files:

  • src/synthorg/api/openapi.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Type hints: all public functions, mypy strict mode
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules)
Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence)
Config vs runtime state: frozen Pydantic models for config/identity; separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves
Never mix static config fields with mutable runtime fields in one model
Models: Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use @computed_field for derived values instead of storing + validating redundant fields
Use NotBlankStr (from core.types) for all identifier/name fields — including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants — instead of manual whitespace validators
Async concurrency: prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task
Functions: < 50 lines, files < 800 lines
Errors: handle explicitly, never silently swallow
Validate: at system boundaries (user input, external APIs, config files)

Files:

  • src/synthorg/api/openapi.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(__name__)
Never use import logging / logging.getLogger() / print() in application code
Variable name: always logger (not _logger, not log)
Event names: always use constants from the domain-specific module under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT
Structured kwargs: always logger.info(EVENT, key=value) — never logger.info("msg %s", val)
All error paths must log at WARNING or ERROR with context before raising
All state transitions must log at INFO
DEBUG for object creation, internal flow, entry/exit of key functions
API reference: auto-generated from docstrings via mkdocstrings + Griffe (AST-based, no imports); document all public APIs with Google-style docstrings
Observability: use structured logging via get_logger() and observability.events constants; correlation tracking for multi-agent workflows

Files:

  • src/synthorg/api/openapi.py
{src,tests}/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Vendor-agnostic everywhere: NEVER use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001, large/medium/small as aliases

Files:

  • src/synthorg/api/openapi.py
src/synthorg/{api,auth,backup,budget,cli,communication,config,core,engine,hr,memory,persistence,observability,providers,security,settings,templates,tools}/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Google style docstrings for all public classes/functions in the API subpackage and other modules; includes parameter, return, raises, and example sections

Files:

  • src/synthorg/api/openapi.py
src/synthorg/api/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

REST API: RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail); use problem detail responses for all API errors

Files:

  • src/synthorg/api/openapi.py
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T11:08:24.271Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail); use problem detail responses for all API errors
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T11:08:24.271Z
Learning: Applies to src/synthorg/{api,auth,backup,budget,cli,communication,config,core,engine,hr,memory,persistence,observability,providers,security,settings,templates,tools}/**/*.py : Google style docstrings for all public classes/functions in the API subpackage and other modules; includes parameter, return, raises, and example sections
📚 Learning: 2026-03-18T11:08:24.271Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T11:08:24.271Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail); use problem detail responses for all API errors

Applied to files:

  • src/synthorg/api/openapi.py
📚 Learning: 2026-03-18T11:08:24.271Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T11:08:24.271Z
Learning: Applies to src/synthorg/{api,auth,backup,budget,cli,communication,config,core,engine,hr,memory,persistence,observability,providers,security,settings,templates,tools}/**/*.py : Google style docstrings for all public classes/functions in the API subpackage and other modules; includes parameter, return, raises, and example sections

Applied to files:

  • src/synthorg/api/openapi.py
🔇 Additional comments (1)
src/synthorg/api/openapi.py (1)

83-165: ASCII-only prose cleanup looks safe.

These edits are documentation-only: they change emitted descriptions/comments without changing the response shape, content negotiation, or example payloads.

Also applies to: 202-202

Replace misleading "idempotent" justification with the actual
project-specific invariant: DELETE endpoints are unconditional
removals with no state preconditions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aureliolo Aureliolo merged commit 71cc6e1 into main Mar 18, 2026
11 of 13 checks passed
@Aureliolo Aureliolo deleted the refactor/error-responses-typeddict branch March 18, 2026 19:09
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 18, 2026 19:09 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Mar 18, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.3.5](v0.3.4...v0.3.5)
(2026-03-18)


### Features

* **api:** auto-wire backend services at startup
([#555](#555))
([0e52c47](0e52c47))


### Bug Fixes

* **api:** resolve WebSocket 403 rejection
([#549](#549))
([#556](#556))
([60453d2](60453d2))
* **cli:** verify SLSA provenance via GitHub attestation API
([#548](#548))
([91d4f79](91d4f79)),
closes [#532](#532)


### Performance

* **test:** speed up test suite -- reduce Hypothesis examples and
eliminate real sleeps
([#557](#557))
([d5f3a41](d5f3a41))


### Refactoring

* replace _ErrorResponseSpec NamedTuple with TypedDict
([#554](#554))
([71cc6e1](71cc6e1))


### Maintenance

* **docker:** suppress pydantic v1 warning on Python 3.14
([#552](#552))
([cbe1f05](cbe1f05)),
closes [#551](#551)

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: replace _ERROR_RESPONSES tuple-of-tuples with TypedDict in openapi.py

1 participant