Skip to content

fix: resolve OpenAPI schema validation warnings for union/optional fields#558

Merged
Aureliolo merged 7 commits intomainfrom
fix/openapi-schema-warnings
Mar 19, 2026
Merged

fix: resolve OpenAPI schema validation warnings for union/optional fields#558
Aureliolo merged 7 commits intomainfrom
fix/openapi-schema-warnings

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Flatten Litestar's nullable oneOf unions to idiomatic JSON Schema 2020-12 type arrays, eliminating 17 of 18 Scalar UI "Expected union value" warnings
  • Inline nullable enum $ref targets (e.g. AutonomyLevel, TaskStructure) by merging enum values into the type array
  • Convert remaining object $ref nullables from oneOf to anyOf (1 remaining warning on MeetingRecord -- confirmed Scalar bug scalar/scalar#8369)
  • Collapse redundant oneOf: [{$ref}, {}] unions emitted by Litestar for tuple[T, ...] items
  • Move RFC 9457 error documentation from info.description to info.x-documentation to prevent Scalar from rendering it at the top of the API reference page

Closes #268

Test plan

  • 16 new unit tests covering all normalization paths (primitive, enum $ref, object $ref, discriminated union, redundant union, multi-branch, idempotency, full pipeline)
  • 2 new integration tests (regression guard scanning full schema, x-documentation assertion)
  • All 9449 existing tests pass unchanged
  • 93.92% coverage (80% required)
  • mypy strict, ruff lint+format clean
  • Manual Scalar UI verification: 18 warnings -> 1 (Scalar bug)
  • Pre-reviewed by 7 agents, 9 findings addressed

🤖 Generated with Claude Code

Aureliolo and others added 2 commits March 18, 2026 22:50
…elds

Flatten Litestar's nullable oneOf unions to idiomatic JSON Schema
2020-12 type arrays, eliminating 17 of 18 Scalar UI "Expected union
value" warnings. The remaining 1 (MeetingRecord) is a confirmed
Scalar bug (scalar/scalar#8369) with no client-side workaround.

- Primitive nullable (T | None): flatten to type: [T, "null"]
- Enum $ref nullable: inline enum values and flatten
- Object $ref nullable: convert oneOf to anyOf
- Redundant oneOf with empty schema: collapse to concrete branch
- Move RFC 9457 docs from info.description to info.x-documentation

Closes #268

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract TestNullableUnionNormalization to test_openapi_nullable.py
  (test_openapi.py was 808 lines, now 647 -- under 800 limit)
- Re-fetch items list after _flatten_nullable to prevent stale ref
- Add clarifying comment about mutation contract on helper functions
- Improve all_schemas docstring wording
- Add 4 new edge-case tests: non-enum $ref with registry, non-
  component $ref prefix, multi-branch union, enum without description

Pre-reviewed by 7 agents, 9 findings addressed

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

@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 significantly enhances the generated OpenAPI schema by normalizing how nullable and union types are represented. The primary goal is to resolve numerous validation warnings in API documentation tools like Scalar UI, making the API reference more accurate and user-friendly. These changes involve transforming complex union structures into more standard JSON Schema formats, improving compatibility and readability without altering the API's functional behavior.

Highlights

  • OpenAPI Schema Normalization: Flattened Litestar's nullable oneOf unions to idiomatic JSON Schema 2020-12 type arrays, significantly reducing Scalar UI validation warnings.
  • Enum Reference Inlining: Inlined nullable enum $ref targets by merging enum values directly into the type array for improved schema clarity.
  • Object Reference Handling: Converted remaining object $ref nullables from oneOf to anyOf to address specific compatibility issues with Scalar UI.
  • Redundant Union Collapse: Collapsed redundant oneOf: [{$ref}, {}] unions, which Litestar emitted for tuple[T, ...] items, simplifying the schema.
  • Documentation Placement: Moved RFC 9457 error documentation from info.description to info.x-documentation to prevent it from being prominently displayed at the top of the API reference page in Scalar UI.
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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 18, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Normalize nullable-union handling in generated OpenAPI schemas: collapse primitive nullable unions into type arrays, inline nullable enums (including None), convert certain $ref nullables to anyOf, and remove redundant unions during final schema post-processing.
  • Tests

    • Added unit and integration tests validating nullable-union normalization, enum inlining, collapse cases, idempotence, and that no oneOf with null remains after processing.
  • Chores

    • Move RFC 9457 guidance out of info.description into info.x-documentation.rfc9457.

Walkthrough

Adds a nullable-union normalization pass to OpenAPI post-processing and moves RFC 9457 documentation out of info.description into info.x-documentation["rfc9457"]. Tests updated to validate normalization (no oneOf with null) and the new documentation location.

Changes

Cohort / File(s) Summary
OpenAPI post-processing
src/synthorg/api/openapi.py
Adds helpers to normalize nullable unions (oneOf/anyOf) into nullable type/enum forms, collapse redundant unions, and integrate this pass into inject_rfc9457_responses; moves RFC9457 text into info.x-documentation["rfc9457"] (creates/uses x-documentation).
Nullable-union unit tests
tests/unit/api/test_openapi_nullable.py
New comprehensive unit tests validating primitive and $ref nullable flattening, enum inlining, anyOf fallback for object refs, nested/property normalization, redundant-union collapse, idempotence, and an end-to-end pipeline check using the post-processor.
OpenAPI unit tests (x-documentation)
tests/unit/api/test_openapi.py
Updated tests to assert RFC9457 is stored under info.x-documentation.rfc9457 and not appended to info.description; renamed and adjusted expectations accordingly.
Integration tests & validators
tests/integration/api/test_openapi_integration.py
Updated RFC9457 assertion to check info.x-documentation; added helper to find oneOf entries containing null and a test to assert no such oneOf remains after processing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: resolving OpenAPI schema validation warnings for union/optional fields, which matches the core objective of flattening nullable unions and normalizing OpenAPI schemas.
Description check ✅ Passed The description is directly related to the changeset, detailing the specific improvements made (flattening unions, inlining enums, moving RFC 9457 docs) and providing comprehensive test coverage information.
Linked Issues check ✅ Passed All coding requirements from issue #268 are met: nullable unions are flattened to type arrays (17 of 18 warnings eliminated) [#268], enum refs are inlined [#268], object refs converted to anyOf [#268], and redundant unions collapsed [#268].
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #268 objectives: nullable union normalization in openapi.py, RFC 9457 documentation relocation, and comprehensive test coverage for validation. No unrelated changes detected.
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 fix/openapi-schema-warnings
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/openapi-schema-warnings
📝 Coding Plan
  • Generate coding plan for human review comments

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

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 18, 2026 22:20 — 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 introduces a series of improvements to the OpenAPI schema generation, primarily to resolve validation warnings in tools like Scalar UI. The changes involve flattening nullable unions, inlining enum references, and moving documentation to a more appropriate extension field. The implementation is well-structured and accompanied by a comprehensive suite of new unit and integration tests, which is excellent. I have one suggestion to improve the robustness of how the x-documentation field is updated.

Comment on lines +611 to +612
if "x-documentation" not in info:
info["x-documentation"] = {"rfc9457": _RFC9457_DESCRIPTION_SECTION}
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

The current implementation for adding x-documentation is not fully robust. If the x-documentation key already exists in the info dictionary (perhaps added by another process) but doesn't contain the rfc9457 key, this function will do nothing, and the RFC 9457 documentation will be missing from the schema.

To make this function more robust and idempotent, you could use setdefault to ensure the nested structure is correctly populated.

        x_doc = info.setdefault("x-documentation", {})
        x_doc.setdefault("rfc9457", _RFC9457_DESCRIPTION_SECTION)

Address Gemini Code Assist review feedback -- the previous check
would silently skip adding rfc9457 if x-documentation already
existed without the rfc9457 key.

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

codecov bot commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 98.50746% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.57%. Comparing base (ae01b06) to head (79c6803).
⚠️ Report is 9 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/synthorg/api/openapi.py 98.50% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #558      +/-   ##
==========================================
+ Coverage   92.55%   92.57%   +0.01%     
==========================================
  Files         544      544              
  Lines       26810    26872      +62     
  Branches     2558     2574      +16     
==========================================
+ Hits        24815    24876      +61     
  Misses       1584     1584              
- Partials      411      412       +1     

☔ 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.

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: 3

🤖 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 11-17: The module-level compatibility notes and related
docstrings/comments in src/synthorg/api/openapi.py currently mention a specific
external renderer and bug; update the text in the module docstring and the
comments around the ProblemDetail and content negotiation sections (references:
"ProblemDetail", "info.x-documentation") to use vendor-agnostic wording such as
"external renderer/tool" and "known renderer bug" or "rendering issue" instead
of real vendor names or bug IDs; keep any tool-specific identifiers only in the
PR/issue tracker, not in code comments or docstrings, and apply the same
replacement to the other occurrences noted (the comment blocks corresponding to
the ranges around 294-317 and 624-655).
- Around line 604-613: The current _update_info_description skips adding rfc9457
when info["x-documentation"] already exists; change it to merge instead: ensure
info.get("x-documentation") is a dict (if missing or not a dict,
replace/initialize it with an empty dict), then set
info["x-documentation"]["rfc9457"] = _RFC9457_DESCRIPTION_SECTION so we add the
RFC9457 content without clobbering other existing keys.

In `@tests/unit/api/test_openapi_nullable.py`:
- Around line 110-123: The docstring for
test_object_ref_anyof_with_registry_stays_anyof currently mentions a specific
vendor; change it to a vendor-agnostic description (e.g., "anyOf with non-enum
$ref + null stays anyOf when registry provided" or "compatibility with external
renderer/registry") so it avoids naming third-party renderers—update the
docstring text only in the test_object_ref_anyof_with_registry_stays_anyof
function to be generic while preserving the test intent.
🪄 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: c7fa543e-71db-4051-af3c-4540a17f71a7

📥 Commits

Reviewing files that changed from the base of the PR and between ae01b06 and b6bf7b4.

📒 Files selected for processing (4)
  • src/synthorg/api/openapi.py
  • tests/integration/api/test_openapi_integration.py
  • tests/unit/api/test_openapi.py
  • tests/unit/api/test_openapi_nullable.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: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Test (Python 3.14)
  • 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
Use PEP 758 except syntax: use except A, B: (no parentheses) — ruff enforces this on Python 3.14
Type hints required on all public functions, mypy strict mode
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating 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, use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence)
Use frozen Pydantic models for config/identity; use 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
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict); use @computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr for all identifier/name fields including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants instead of manual whitespace validators
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
Line length: 88 characters (ruff)
Functions should be < 50 lines, files < 800 lines
Validate at system boundaries (user input, external APIs, config files)

Files:

  • tests/unit/api/test_openapi_nullable.py
  • tests/integration/api/test_openapi_integration.py
  • src/synthorg/api/openapi.py
  • tests/unit/api/test_openapi.py
**/{*.py,*.go}

📄 CodeRabbit inference engine (CLAUDE.md)

Handle errors explicitly, never silently swallow them

Files:

  • tests/unit/api/test_openapi_nullable.py
  • tests/integration/api/test_openapi_integration.py
  • src/synthorg/api/openapi.py
  • tests/unit/api/test_openapi.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Test markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow
Async testing: asyncio_mode = "auto" — no manual @pytest.mark.asyncio needed
Prefer @pytest.mark.parametrize for testing similar cases
Never skip, dismiss, or ignore flaky tests — always fix them fully and fundamentally. For timing-sensitive tests, mock time.monotonic() and asyncio.sleep() to make them deterministic instead of widening timing margins

Files:

  • tests/unit/api/test_openapi_nullable.py
  • tests/integration/api/test_openapi_integration.py
  • tests/unit/api/test_openapi.py
{src/synthorg/**/*.py,tests/**/*.py,**/*.md,web/**/{*.ts,*.js,*.vue}}

📄 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. Vendor names only in: (1) Operations design page, (2) .claude/ files, (3) third-party import paths/modules

Files:

  • tests/unit/api/test_openapi_nullable.py
  • tests/integration/api/test_openapi_integration.py
  • src/synthorg/api/openapi.py
  • tests/unit/api/test_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 for logger: always logger (not _logger, not log)
Event names: always use constants from domain-specific modules 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
Use structured logging: 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 level
DEBUG level logging for object creation, internal flow, entry/exit of key functions
Library reference: auto-generated from docstrings via mkdocstrings + Griffe (AST-based, no imports) in docs/api/
Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP, tool factory, sandbox factory)

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers

Files:

  • src/synthorg/api/openapi.py
🧬 Code graph analysis (3)
tests/unit/api/test_openapi_nullable.py (1)
src/synthorg/api/openapi.py (2)
  • _normalize_nullable_unions (290-339)
  • inject_rfc9457_responses (618-664)
tests/integration/api/test_openapi_integration.py (1)
src/synthorg/api/openapi.py (1)
  • inject_rfc9457_responses (618-664)
tests/unit/api/test_openapi.py (1)
src/synthorg/api/openapi.py (1)
  • inject_rfc9457_responses (618-664)
🔇 Additional comments (3)
tests/integration/api/test_openapi_integration.py (1)

67-103: Nice schema-wide regression guard.

Scanning the processed document for surviving oneOf + null pairs should keep this test effective as new optional models are added.

tests/unit/api/test_openapi_nullable.py (1)

213-257: Good coverage on idempotency and the end-to-end path.

These cases should catch both local helper regressions and wiring mistakes in inject_rfc9457_responses.

tests/unit/api/test_openapi.py (1)

588-613: Good regression coverage for the info move.

These assertions pin both sides of the behavior change: rfc9457 is stored under x-documentation, and a pre-existing info.description stays untouched.

Comment on lines +110 to +123
def test_object_ref_becomes_anyof(self) -> None:
"""$ref to object + null uses anyOf (Scalar bug #8369)."""
schema: dict[str, Any] = {
"oneOf": [
{"$ref": "#/components/schemas/Minutes"},
{"type": "null"},
],
}
result = _normalize_nullable_unions(schema)
assert "anyOf" in result
assert "oneOf" not in result

def test_object_ref_anyof_with_registry_stays_anyof(self) -> None:
"""anyOf with non-enum $ref + null stays anyOf when registry provided."""
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep the test wording vendor-agnostic too.

This new docstring names a specific third-party renderer. Please describe it as a generic compatibility case so the test suite follows the same rule as application code. As per coding guidelines, "Vendor-agnostic everywhere: NEVER use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/api/test_openapi_nullable.py` around lines 110 - 123, The
docstring for test_object_ref_anyof_with_registry_stays_anyof currently mentions
a specific vendor; change it to a vendor-agnostic description (e.g., "anyOf with
non-enum $ref + null stays anyOf when registry provided" or "compatibility with
external renderer/registry") so it avoids naming third-party renderers—update
the docstring text only in the test_object_ref_anyof_with_registry_stays_anyof
function to be generic while preserving the test intent.

Replace "Scalar UI" and "Scalar bug #8369" with vendor-agnostic
wording per project conventions. Tool-specific context is preserved
in the PR and issue tracker.

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.

♻️ Duplicate comments (2)
src/synthorg/api/openapi.py (2)

611-612: ⚠️ Potential issue | 🟠 Major

Guard info["x-documentation"] type before setdefault call.

Line 611 can return a non-dict existing value; Line 612 would then raise AttributeError on setdefault. Handle non-dict values defensively before insertion.

💡 Proposed fix
 def _update_info_description(info: dict[str, Any]) -> None:
@@
-    x_doc: dict[str, Any] = info.setdefault("x-documentation", {})
-    x_doc.setdefault("rfc9457", _RFC9457_DESCRIPTION_SECTION)
+    x_doc_raw = info.get("x-documentation")
+    if not isinstance(x_doc_raw, dict):
+        x_doc_raw = {}
+        info["x-documentation"] = x_doc_raw
+    x_doc_raw.setdefault("rfc9457", _RFC9457_DESCRIPTION_SECTION)
🤖 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 611 - 612, The code assumes
info["x-documentation"] is a dict before calling setdefault; if it exists but is
not a dict, x_doc.setdefault will raise AttributeError. Update the logic around
info.setdefault("x-documentation", {}) so you first read
info.get("x-documentation"), check isinstance(value, dict), and if not replace
it with a new dict (or log/convert) before assigning to x_doc; then call
x_doc.setdefault("rfc9457", _RFC9457_DESCRIPTION_SECTION). Ensure this change
touches the variables/info access near x_doc and references
_RFC9457_DESCRIPTION_SECTION.

11-17: ⚠️ Potential issue | 🟡 Minor

Keep OpenAPI compatibility notes vendor-agnostic.

These comments/docstrings still reference a specific renderer and bug link. Please rewrite them to generic wording (for example, “external renderer” / “known rendering issue”) and keep tool-specific identifiers in PR/issue context only.

As per coding guidelines, "Vendor-agnostic everywhere: NEVER use real vendor names ... in project-owned code, docstrings, comments, tests, or config examples."

Also applies to: 297-317, 605-610, 624-626, 653-655

🤖 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 11 - 17, Several module docstrings
and comments in src/synthorg/api/openapi.py reference a specific renderer and a
bug link; update these to be vendor-agnostic by replacing the vendor/tool name
and bug URL with generic wording such as "external renderer" or "known rendering
issue" and keep any tool-specific identifiers only in PR/issue context. Locate
the OpenAPI compatibility notes and other related docstrings that mention
ProblemDetail, info.x-documentation, and the behavior of the renderer (the
sections around the top-of-file notes and the other comment blocks referenced)
and change their language to be generic while preserving the explanation (e.g.,
“known rendering issue” instead of the vendor name, “external renderer” instead
of product name). Ensure all instances in the noted comment ranges are updated
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/synthorg/api/openapi.py`:
- Around line 611-612: The code assumes info["x-documentation"] is a dict before
calling setdefault; if it exists but is not a dict, x_doc.setdefault will raise
AttributeError. Update the logic around info.setdefault("x-documentation", {})
so you first read info.get("x-documentation"), check isinstance(value, dict),
and if not replace it with a new dict (or log/convert) before assigning to
x_doc; then call x_doc.setdefault("rfc9457", _RFC9457_DESCRIPTION_SECTION).
Ensure this change touches the variables/info access near x_doc and references
_RFC9457_DESCRIPTION_SECTION.
- Around line 11-17: Several module docstrings and comments in
src/synthorg/api/openapi.py reference a specific renderer and a bug link; update
these to be vendor-agnostic by replacing the vendor/tool name and bug URL with
generic wording such as "external renderer" or "known rendering issue" and keep
any tool-specific identifiers only in PR/issue context. Locate the OpenAPI
compatibility notes and other related docstrings that mention ProblemDetail,
info.x-documentation, and the behavior of the renderer (the sections around the
top-of-file notes and the other comment blocks referenced) and change their
language to be generic while preserving the explanation (e.g., “known rendering
issue” instead of the vendor name, “external renderer” instead of product name).
Ensure all instances in the noted comment ranges are updated consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b8ec83cc-e79f-4e26-9cd7-c6b5669ea3c8

📥 Commits

Reviewing files that changed from the base of the PR and between b6bf7b4 and 0c50822.

📒 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: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations
Use PEP 758 except syntax: use except A, B: (no parentheses) — ruff enforces this on Python 3.14
Type hints required on all public functions, mypy strict mode
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating 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, use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence)
Use frozen Pydantic models for config/identity; use 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
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict); use @computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr for all identifier/name fields including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants instead of manual whitespace validators
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
Line length: 88 characters (ruff)
Functions should be < 50 lines, files < 800 lines
Validate at system boundaries (user input, external APIs, config files)

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

Handle errors explicitly, never silently swallow them

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 for logger: always logger (not _logger, not log)
Event names: always use constants from domain-specific modules 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
Use structured logging: 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 level
DEBUG level logging for object creation, internal flow, entry/exit of key functions
Library reference: auto-generated from docstrings via mkdocstrings + Griffe (AST-based, no imports) in docs/api/
Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP, tool factory, sandbox factory)

Files:

  • src/synthorg/api/openapi.py
{src/synthorg/**/*.py,tests/**/*.py,**/*.md,web/**/{*.ts,*.js,*.vue}}

📄 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. Vendor names only in: (1) Operations design page, (2) .claude/ files, (3) third-party import paths/modules

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers

Files:

  • src/synthorg/api/openapi.py
🧠 Learnings (2)
📚 Learning: 2026-03-18T21:35:45.198Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T21:35:45.198Z
Learning: Applies to {src/synthorg/**/*.py,tests/**/*.py,**/*.md,web/**/{*.ts,*.js,*.vue}} : 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. Vendor names only in: (1) Operations design page, (2) `.claude/` files, (3) third-party import paths/modules

Applied to files:

  • src/synthorg/api/openapi.py
📚 Learning: 2026-03-15T11:48:14.867Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T11:48:14.867Z
Learning: Applies to {src,tests,web,cli,site}/**/*.{py,ts,tsx,go,astro} : 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. Vendor names may only appear in: (1) Operations design page provider list (docs/design/operations.md), (2) .claude/ skill/agent files, (3) third-party import paths/module names.

Applied to files:

  • src/synthorg/api/openapi.py

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.

♻️ Duplicate comments (1)
src/synthorg/api/openapi.py (1)

611-612: ⚠️ Potential issue | 🟠 Major

Guard x-documentation type before calling setdefault.

At Line 611, if info["x-documentation"] exists but is not a dict, Line 612 will raise at runtime. Re-initialize non-dict values to {} before writing rfc9457.

💡 Proposed fix
 def _update_info_description(info: dict[str, Any]) -> None:
@@
-    x_doc: dict[str, Any] = info.setdefault("x-documentation", {})
-    x_doc.setdefault("rfc9457", _RFC9457_DESCRIPTION_SECTION)
+    x_doc = info.get("x-documentation")
+    if not isinstance(x_doc, dict):
+        x_doc = {}
+        info["x-documentation"] = x_doc
+    x_doc.setdefault("rfc9457", _RFC9457_DESCRIPTION_SECTION)
🤖 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 611 - 612, Guard the existing
info["x-documentation"] value before calling setdefault: check if
info.get("x-documentation") is a dict and if not replace it with a new dict,
then assign x_doc = info.setdefault("x-documentation", {}) and call
x_doc.setdefault("rfc9457", _RFC9457_DESCRIPTION_SECTION); update the logic
around the x_doc assignment so non-dict values are re-initialized to {} before
adding the rfc9457 entry (referencing the x_doc variable, info.setdefault call,
and _RFC9457_DESCRIPTION_SECTION).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/synthorg/api/openapi.py`:
- Around line 611-612: Guard the existing info["x-documentation"] value before
calling setdefault: check if info.get("x-documentation") is a dict and if not
replace it with a new dict, then assign x_doc =
info.setdefault("x-documentation", {}) and call x_doc.setdefault("rfc9457",
_RFC9457_DESCRIPTION_SECTION); update the logic around the x_doc assignment so
non-dict values are re-initialized to {} before adding the rfc9457 entry
(referencing the x_doc variable, info.setdefault call, and
_RFC9457_DESCRIPTION_SECTION).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 327307ab-ed8f-43af-b4e4-b90dd8ad14af

📥 Commits

Reviewing files that changed from the base of the PR and between 0c50822 and 7464a72.

📒 Files selected for processing (2)
  • src/synthorg/api/openapi.py
  • tests/unit/api/test_openapi_nullable.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 Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • 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
Use PEP 758 except syntax: use except A, B: (no parentheses) — ruff enforces this on Python 3.14
Type hints required on all public functions, mypy strict mode
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating 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, use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence)
Use frozen Pydantic models for config/identity; use 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
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict); use @computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr for all identifier/name fields including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants instead of manual whitespace validators
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
Line length: 88 characters (ruff)
Functions should be < 50 lines, files < 800 lines
Validate at system boundaries (user input, external APIs, config files)

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

Handle errors explicitly, never silently swallow them

Files:

  • src/synthorg/api/openapi.py
  • tests/unit/api/test_openapi_nullable.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 for logger: always logger (not _logger, not log)
Event names: always use constants from domain-specific modules 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
Use structured logging: 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 level
DEBUG level logging for object creation, internal flow, entry/exit of key functions
Library reference: auto-generated from docstrings via mkdocstrings + Griffe (AST-based, no imports) in docs/api/
Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP, tool factory, sandbox factory)

Files:

  • src/synthorg/api/openapi.py
{src/synthorg/**/*.py,tests/**/*.py,**/*.md,web/**/{*.ts,*.js,*.vue}}

📄 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. Vendor names only in: (1) Operations design page, (2) .claude/ files, (3) third-party import paths/modules

Files:

  • src/synthorg/api/openapi.py
  • tests/unit/api/test_openapi_nullable.py
src/synthorg/api/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Test markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow
Async testing: asyncio_mode = "auto" — no manual @pytest.mark.asyncio needed
Prefer @pytest.mark.parametrize for testing similar cases
Never skip, dismiss, or ignore flaky tests — always fix them fully and fundamentally. For timing-sensitive tests, mock time.monotonic() and asyncio.sleep() to make them deterministic instead of widening timing margins

Files:

  • tests/unit/api/test_openapi_nullable.py
🧠 Learnings (3)
📚 Learning: 2026-03-18T21:35:45.198Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T21:35:45.198Z
Learning: Applies to {src/synthorg/**/*.py,tests/**/*.py,**/*.md,web/**/{*.ts,*.js,*.vue}} : 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. Vendor names only in: (1) Operations design page, (2) `.claude/` files, (3) third-party import paths/modules

Applied to files:

  • src/synthorg/api/openapi.py
  • tests/unit/api/test_openapi_nullable.py
📚 Learning: 2026-03-15T11:48:14.867Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T11:48:14.867Z
Learning: Applies to {src,tests,web,cli,site}/**/*.{py,ts,tsx,go,astro} : 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. Vendor names may only appear in: (1) Operations design page provider list (docs/design/operations.md), (2) .claude/ skill/agent files, (3) third-party import paths/module names.

Applied to files:

  • src/synthorg/api/openapi.py
  • tests/unit/api/test_openapi_nullable.py
📚 Learning: 2026-03-15T11:48:14.867Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T11:48:14.867Z
Learning: Applies to tests/**/*.py : Tests must use test-provider, test-small-001, etc. for vendor-agnostic test data.

Applied to files:

  • tests/unit/api/test_openapi_nullable.py
🧬 Code graph analysis (1)
tests/unit/api/test_openapi_nullable.py (1)
src/synthorg/api/openapi.py (2)
  • _normalize_nullable_unions (290-339)
  • inject_rfc9457_responses (618-664)
🔇 Additional comments (2)
src/synthorg/api/openapi.py (1)

189-339: Robust normalization design and immutability handling look good.

The recursive normalization pipeline is well-structured, and the deep copy at Line 638 keeps caller input untouched while enabling safe in-place updates on derived objects.

As per coding guidelines, "Create new objects instead of mutating existing ones."

Also applies to: 638-638

tests/unit/api/test_openapi_nullable.py (1)

39-257: Comprehensive regression coverage for normalization behavior.

This test suite validates the critical transformation paths (primitive/enum/object refs, redundant unions, nested properties, idempotency, and full pipeline), which materially reduces regression risk for the new post-processing logic.

Handle edge case where info["x-documentation"] exists but is not
a dict -- re-initialize to empty dict before inserting rfc9457 key.

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: 2

🤖 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 294-299: Fix the small grammar mistake in the openapi.py docstring
that describes nullable union schema flattening: change the sentence "API doc
renderers expects the compact ..." to "API doc renderers expect the compact ..."
in the docstring for the function that flattens nullable union schemas (the
docstring starting "Flatten nullable union schemas to idiomatic JSON Schema
2020-12.").
- Around line 656-658: The comment above the call to _normalize_nullable_unions
has a duplicated word; update the comment to remove the extra "workaround" so it
reads something like "Workaround for Renderer bug -- see issue `#268` for details"
(or simply "Workaround for Renderer bug") while leaving the call to
_normalize_nullable_unions(result, all_schemas=schemas) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 24f81bba-a07d-4d4e-b8eb-530c338eb3de

📥 Commits

Reviewing files that changed from the base of the PR and between 7464a72 and d455b6d.

📒 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). (4)
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations
Use PEP 758 except syntax: use except A, B: (no parentheses) — ruff enforces this on Python 3.14
Type hints required on all public functions, mypy strict mode
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating 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, use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence)
Use frozen Pydantic models for config/identity; use 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
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict); use @computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr for all identifier/name fields including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants instead of manual whitespace validators
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
Line length: 88 characters (ruff)
Functions should be < 50 lines, files < 800 lines
Validate at system boundaries (user input, external APIs, config files)

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

Handle errors explicitly, never silently swallow them

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 for logger: always logger (not _logger, not log)
Event names: always use constants from domain-specific modules 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
Use structured logging: 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 level
DEBUG level logging for object creation, internal flow, entry/exit of key functions
Library reference: auto-generated from docstrings via mkdocstrings + Griffe (AST-based, no imports) in docs/api/
Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP, tool factory, sandbox factory)

Files:

  • src/synthorg/api/openapi.py
{src/synthorg/**/*.py,tests/**/*.py,**/*.md,web/**/{*.ts,*.js,*.vue}}

📄 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. Vendor names only in: (1) Operations design page, (2) .claude/ files, (3) third-party import paths/modules

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers

Files:

  • src/synthorg/api/openapi.py
🧠 Learnings (2)
📚 Learning: 2026-03-18T21:35:45.198Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T21:35:45.198Z
Learning: Applies to {src/synthorg/**/*.py,tests/**/*.py,**/*.md,web/**/{*.ts,*.js,*.vue}} : 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. Vendor names only in: (1) Operations design page, (2) `.claude/` files, (3) third-party import paths/modules

Applied to files:

  • src/synthorg/api/openapi.py
📚 Learning: 2026-03-15T11:48:14.867Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T11:48:14.867Z
Learning: Applies to {src,tests,web,cli,site}/**/*.{py,ts,tsx,go,astro} : 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. Vendor names may only appear in: (1) Operations design page provider list (docs/design/operations.md), (2) .claude/ skill/agent files, (3) third-party import paths/module names.

Applied to files:

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

11-17: LGTM!

The module docstring now uses vendor-agnostic language ("API doc renderers") as required by guidelines.


179-287: LGTM!

The nullable union normalization helpers are well-structured with clear separation of concerns:

  • _flatten_nullable_ref handles enum $ref inlining
  • _flatten_nullable dispatches based on branch type
  • _collapse_redundant_union handles Litestar's tuple item edge case

The identity comparison (is not) in the list comprehensions is intentional and correct for filtering mutable dict objects.


604-615: LGTM!

The implementation now correctly handles all edge cases:

  • Creates x-documentation if missing
  • Re-initializes if it exists but isn't a dict
  • Uses setdefault to preserve existing rfc9457 content

- "expects" -> "expect" (subject-verb agreement)
- Remove duplicated "workaround" in comment

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: 2

🤖 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 290-339: The current _normalize_nullable_unions walks the entire
OpenAPI dict and mutates any dict/list containing oneOf/anyOf (including
examples), so change the flow to only normalize actual Schema Object locations:
call _normalize_nullable_unions only on components.schemas entries and on every
Schema Object found at parameter/header.schema, requestBody.content.*.schema,
response.content.*.schema, and callback/webhook body schema locations (and not
on full document or example/extension values). Implement a small walker in
inject_rfc9457_responses (or a new helper) that iterates these known locations
and invokes _normalize_nullable_unions (which can remain schema-focused and call
_flatten_nullable/_collapse_redundant_union), leaving all other parts of the
OpenAPI dict untouched so examples and extensions are not modified.
- Around line 268-288: The current _collapse_redundant_union function
incorrectly replaces any oneOf/anyOf containing {} with the concrete branch
(using result.update(concrete[0])), which changes JSON Schema semantics;
instead, stop the generic collapse and only rewrite when you can confidently
detect Litestar's tuple encoding pattern (inspect keyword, items, and use
_EXPECTED_UNION_BRANCHES to ensure shape) — for example, match the specific
container structure Litestar emits for tuples and transform that structure only,
otherwise return without modifying result or deleting result[keyword]; preserve
behavior for unknown schemas to avoid changing validation semantics.
🪄 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: 73e36e07-8a93-4ef6-8f5b-7ad6a112dc1e

📥 Commits

Reviewing files that changed from the base of the PR and between d455b6d and 2a0ded3.

📒 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). (4)
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations.
Use except A, B: syntax (no parentheses) for exception handling — PEP 758 except syntax, enforced by ruff on Python 3.14.
All public functions require type hints — mypy strict mode enforced.
Docstrings must use Google style and are required on all public classes and functions — enforced by ruff D rules.

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Use 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: use 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.
Use 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.
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.
Line length: 88 characters (ruff).
Functions must be less than 50 lines; files must be less than 800 lines.
Handle errors explicitly, never silently swallow them.
Validate at system boundaries (user input, external APIs, config files).
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. Tests must use test-provider, test-small-001, etc.

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.
Always use logger as the variable name (not _logger, not log).
Event names must always use constants from the domain-specific module under synthorg.observability.events. Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.
Use structured kwargs in logger calls: 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 level.
DEBUG logging for object creation, internal flow, entry/exit of key functions.
All provider calls go through BaseCompletionProvider which applies retry + rate limiting automatically. Never implement retry logic in driver subclasses or calling code.
RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig.
Retryable errors (is_retryable=True) include: RateLimitError, ProviderTimeoutError, ProviderConnectionError, ProviderInternalError. Non-retryable errors raise immediately.
RetryExhaustedError signals that all retries failed — the engine layer catches this to trigger fallback chains.

Files:

  • src/synthorg/api/openapi.py
🧠 Learnings (2)
📚 Learning: 2026-03-15T11:48:14.867Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T11:48:14.867Z
Learning: Applies to {src,tests,web,cli,site}/**/*.{py,ts,tsx,go,astro} : 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. Vendor names may only appear in: (1) Operations design page provider list (docs/design/operations.md), (2) .claude/ skill/agent files, (3) third-party import paths/module names.

Applied to files:

  • src/synthorg/api/openapi.py
📚 Learning: 2026-03-19T07:09:59.660Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:09:59.660Z
Learning: Applies to src/**/*.py : 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. Tests must use test-provider, test-small-001, etc.

Applied to files:

  • src/synthorg/api/openapi.py

Comment on lines +268 to +288
def _collapse_redundant_union(
result: dict[str, Any],
keyword: str,
items: list[Any],
) -> None:
"""Collapse a redundant ``oneOf``/``anyOf`` with an empty schema.

Litestar emits ``oneOf: [{$ref: ...}, {}]`` for tuple item
schemas. The empty ``{}`` matches anything, making the union
redundant -- collapse to just the concrete branch.
"""
if len(items) != _EXPECTED_UNION_BRANCHES:
return
empty_entries = [i for i in items if isinstance(i, dict) and not i]
if len(empty_entries) != 1:
return
concrete = [i for i in items if i is not empty_entries[0]]
if concrete:
del result[keyword]
result.update(concrete[0])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In JSON Schema 2020-12, what are the validation semantics of anyOf: [, {}]andoneOf: [, {}]? Are either of them equivalent to just ?

💡 Result:

In JSON Schema 2020-12, the empty schema object {} is an empty schema and therefore always passes validation (“Always passes validation, as if the empty schema {}”). [1]

anyOf: [ <schema>, {} ]

anyOf is valid if the instance validates against at least one subschema. [2]
Since {} always validates, anyOf: [<schema>, {}] always validates every instance (regardless of whether <schema> validates). Therefore it is not equivalent to <schema> (unless <schema> itself also accepts every instance). [1][2]

oneOf: [ <schema>, {} ]

oneOf is valid if the instance validates against exactly one subschema. [3]
Since {} always validates, oneOf: [<schema>, {}] validates iff <schema> does not validate (because otherwise both branches validate and “exactly one” fails). So it behaves like not: <schema>, not like <schema>. [1][3]

So: neither is equivalent to just <schema> (except in degenerate cases such as <schema> being always-true / always-false). [1][2][3]

Sources:
[1] JSON Schema Core 2020-12 — “empty schema” / boolean schemas (true behaves like {}) https://json-schema.org/draft/2020-12/json-schema-core
[2] JSON Schema Core 2020-12 — anyOf semantics https://json-schema.org/draft/2020-12/json-schema-core
[3] JSON Schema Core 2020-12 — oneOf semantics https://json-schema.org/draft/2020-12/json-schema-core


Do not collapse oneOf/anyOf with {} into the concrete branch.

Per JSON Schema 2020-12, the empty schema {} always validates. This makes anyOf: [<schema>, {}] always valid (not equivalent to <schema>), and oneOf: [<schema>, {}] validate only when <schema> does not (equivalent to not: <schema>). Collapsing either to just the concrete branch using result.update(concrete[0]) changes the validation semantics. If Litestar's tuple representation needs rewriting, detect and rewrite that specific container structure instead of applying this generic rule.

🤖 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 268 - 288, The current
_collapse_redundant_union function incorrectly replaces any oneOf/anyOf
containing {} with the concrete branch (using result.update(concrete[0])), which
changes JSON Schema semantics; instead, stop the generic collapse and only
rewrite when you can confidently detect Litestar's tuple encoding pattern
(inspect keyword, items, and use _EXPECTED_UNION_BRANCHES to ensure shape) — for
example, match the specific container structure Litestar emits for tuples and
transform that structure only, otherwise return without modifying result or
deleting result[keyword]; preserve behavior for unknown schemas to avoid
changing validation semantics.

Comment on lines +290 to +339
def _normalize_nullable_unions(
obj: Any,
all_schemas: dict[str, Any] | None = None,
) -> Any:
"""Flatten nullable union schemas to idiomatic JSON Schema 2020-12.

Litestar wraps ``T | None`` fields in ``oneOf``, producing
``oneOf: [{type: "string"}, {type: "null"}]``. API doc renderers
expect the compact ``type: ["string", "null"]`` form for
primitives, and ``anyOf`` for ``$ref``-based nullables.

Args:
obj: Any JSON-serialisable value (typically the full OpenAPI
schema dict).
all_schemas: ``components.schemas`` dict used to resolve
``$ref`` targets for enum inlining. When ``None``,
``$ref``-based nullable unions are converted to ``anyOf``
(enums cannot be inlined without schema resolution).

Conversion rules (applied to both ``oneOf`` and ``anyOf``):

* **Primitive nullable** -- non-null branch has a ``type`` key:
merge into ``{type: [T, "null"], ...extras}``.
* **Enum $ref nullable** -- non-null branch is a ``$ref`` to a
simple enum: inline the enum values and flatten.
* **Object $ref nullable** -- non-null branch is a ``$ref`` to
a complex schema: convert to ``anyOf`` (known renderer
bug -- see linked issue for details).
* **Redundant union** -- one branch is an empty schema ``{}``:
collapse to just the non-empty branch (Litestar emits this
for ``tuple[T, ...]`` item schemas).
* **Discriminated unions** -- no ``{"type": "null"}`` entry and
no empty-schema branch: left unchanged.
"""
if isinstance(obj, dict):
result = {k: _normalize_nullable_unions(v, all_schemas) for k, v in obj.items()}

for keyword in ("oneOf", "anyOf"):
if keyword not in result or not isinstance(result[keyword], list):
continue
_flatten_nullable(result, keyword, result[keyword], all_schemas)
if keyword in result:
# Re-fetch: _flatten_nullable may have replaced the list.
_collapse_redundant_union(result, keyword, result[keyword])

return result

if isinstance(obj, list):
return [_normalize_nullable_unions(item, all_schemas) for item in obj]
return obj
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In OpenAPI 3.1, which parts of the document are Schema Objects, and are example/examples values arbitrary instance data rather than schema definitions?

💡 Result:

Where Schema Objects appear in an OpenAPI 3.1 document

A Schema Object (i.e., an OpenAPI-flavored wrapper around JSON Schema 2020-12) is used anywhere you see a schema: field, most notably:

  • components.schemas (reusable schemas) (github.com)
  • Parameters and headers
    • Parameter Object.schema (or Parameter Object.content[*].schema) (spec.openapis.org)
    • Header Object.schema (or Header Object.content[*].schema) (github.com)
  • Message bodies via Media Type Objects
    • Request Body Object.content[mediaType].schema (github.com)
    • Response Object.content[mediaType].schema (github.com)
    • (And the same Media Type Object pattern is used anywhere content appears, e.g., in callback/webhook request/response bodies.) (github.com)

Are example / examples arbitrary instance data (not schemas)?

Yes. In OpenAPI 3.1:

  • example (where allowed) is “Any” and is an example instance value expected to match/align with the associated schema. (spec.openapis.org)
  • examples in Parameters / Media Types is a map of Example Objects, whose value is an embedded literal example (instance data), not a schema. (spec.openapis.org)
  • Within the Schema Object itself, examples (JSON Schema’s keyword) is preferred; OpenAPI’s Schema Object example is retained mainly for compatibility and is described as a free-form example of an instance. (github.com)

Citations:


🏁 Script executed:

# First, let's check the file size and read the relevant section
wc -l src/synthorg/api/openapi.py

Repository: Aureliolo/synthorg

Length of output: 93


🏁 Script executed:

# Read the function in question (lines 290-339)
sed -n '290,339p' src/synthorg/api/openapi.py

Repository: Aureliolo/synthorg

Length of output: 2315


🏁 Script executed:

# Also check lines 656-658 mentioned in "Also applies to"
sed -n '650,665p' src/synthorg/api/openapi.py

Repository: Aureliolo/synthorg

Length of output: 603


🏁 Script executed:

# Find where inject_rfc9457_responses() calls this function
rg -n "inject_rfc9457_responses|_normalize_nullable_unions" src/synthorg/api/openapi.py

Repository: Aureliolo/synthorg

Length of output: 624


Restrict schema normalization to Schema Object locations only; do not process the entire OpenAPI document.

_normalize_nullable_unions() recursively processes every dict/list in the input, and inject_rfc9457_responses() (line 658) feeds it the entire OpenAPI document. This means example/examples payloads or extension values that happen to contain oneOf/anyOf will be rewritten as if they were schema definitions, silently corrupting user-supplied instance data. Per OpenAPI 3.1, example/examples are arbitrary instance data, not schema definitions, and should never be transformed.

Instead of walking the full document, restrict normalization to known Schema Object locations:

  • components.schemas
  • Parameter/header schema fields
  • Request/response body media-type schema fields
  • Callback/webhook body schemas

This prevents accidental corruption of example data while still covering all actual schema definitions.

🤖 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 290 - 339, The current
_normalize_nullable_unions walks the entire OpenAPI dict and mutates any
dict/list containing oneOf/anyOf (including examples), so change the flow to
only normalize actual Schema Object locations: call _normalize_nullable_unions
only on components.schemas entries and on every Schema Object found at
parameter/header.schema, requestBody.content.*.schema,
response.content.*.schema, and callback/webhook body schema locations (and not
on full document or example/extension values). Implement a small walker in
inject_rfc9457_responses (or a new helper) that iterates these known locations
and invokes _normalize_nullable_unions (which can remain schema-focused and call
_flatten_nullable/_collapse_redundant_union), leaving all other parts of the
OpenAPI dict untouched so examples and extensions are not modified.

Collapsing anyOf with an empty schema {} would change semantics --
{} matches anything, so anyOf with {} means "accept anything".
Litestar only emits this pattern as oneOf for tuple item schemas.

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.

♻️ Duplicate comments (2)
src/synthorg/api/openapi.py (2)

280-288: ⚠️ Potential issue | 🔴 Critical

Do not collapse oneOf: [X, {}] into X.

Line 287 replaces the union with the concrete branch, but in JSON Schema 2020-12 {} is the always-valid schema, so oneOf: [X, {}] validates the complement of X, not X. This changes the published contract. Only rewrite the specific Litestar tuple shape once you can positively identify it; otherwise leave the union intact.

In JSON Schema 2020-12, what does `oneOf: [<schema>, {}]` validate? Is it equivalent to `<schema>`?

657-659: ⚠️ Potential issue | 🟠 Major

Normalize schema nodes, not the whole OpenAPI document.

Line 659 sends the entire spec through _normalize_nullable_unions(). That helper rewrites any dict/list containing oneOf or anyOf, so user-authored example / examples payloads and extension values can be mutated as if they were Schema Objects. Walk only known schema locations (components.schemas, parameter/header schema, request/response media-type schema, callback/webhook body schema) and leave the rest of the document untouched.

In OpenAPI 3.1, are `example` / `examples` values instance data rather than Schema Objects, and which parts of the document are Schema Objects?
🤖 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 657 - 659, The call currently
passes the entire OpenAPI document into _normalize_nullable_unions(result,
all_schemas=schemas), which mutates non-schema nodes like example/examples and
extensions; instead, change the flow to walk only known schema locations and
call _normalize_nullable_unions on those schema objects: iterate
components.schemas entries, parameter and header.schema objects, request and
response media-type.schema objects, plus callback/webhook body schema nodes, and
apply _normalize_nullable_unions to each schema node (providing
all_schemas=schemas) leaving the rest of result untouched; update any helpers
that batch-normalize to accept single schema nodes or lists of schema nodes so
you don’t feed the whole spec into _normalize_nullable_unions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/synthorg/api/openapi.py`:
- Around line 657-659: The call currently passes the entire OpenAPI document
into _normalize_nullable_unions(result, all_schemas=schemas), which mutates
non-schema nodes like example/examples and extensions; instead, change the flow
to walk only known schema locations and call _normalize_nullable_unions on those
schema objects: iterate components.schemas entries, parameter and header.schema
objects, request and response media-type.schema objects, plus callback/webhook
body schema nodes, and apply _normalize_nullable_unions to each schema node
(providing all_schemas=schemas) leaving the rest of result untouched; update any
helpers that batch-normalize to accept single schema nodes or lists of schema
nodes so you don’t feed the whole spec into _normalize_nullable_unions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9106e7b7-97c8-4c84-95ee-119c8133df58

📥 Commits

Reviewing files that changed from the base of the PR and between 2a0ded3 and 79c6803.

📒 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). (4)
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Backend
  • GitHub Check: Build Sandbox
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations.
Use except A, B: syntax (no parentheses) for exception handling — PEP 758 except syntax, enforced by ruff on Python 3.14.
All public functions require type hints — mypy strict mode enforced.
Docstrings must use Google style and are required on all public classes and functions — enforced by ruff D rules.

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Use 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: use 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.
Use 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.
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.
Line length: 88 characters (ruff).
Functions must be less than 50 lines; files must be less than 800 lines.
Handle errors explicitly, never silently swallow them.
Validate at system boundaries (user input, external APIs, config files).
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. Tests must use test-provider, test-small-001, etc.

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.
Always use logger as the variable name (not _logger, not log).
Event names must always use constants from the domain-specific module under synthorg.observability.events. Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.
Use structured kwargs in logger calls: 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 level.
DEBUG logging for object creation, internal flow, entry/exit of key functions.
All provider calls go through BaseCompletionProvider which applies retry + rate limiting automatically. Never implement retry logic in driver subclasses or calling code.
RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig.
Retryable errors (is_retryable=True) include: RateLimitError, ProviderTimeoutError, ProviderConnectionError, ProviderInternalError. Non-retryable errors raise immediately.
RetryExhaustedError signals that all retries failed — the engine layer catches this to trigger fallback chains.

Files:

  • src/synthorg/api/openapi.py
🧠 Learnings (2)
📚 Learning: 2026-03-15T11:48:14.867Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T11:48:14.867Z
Learning: Applies to {src,tests,web,cli,site}/**/*.{py,ts,tsx,go,astro} : 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. Vendor names may only appear in: (1) Operations design page provider list (docs/design/operations.md), (2) .claude/ skill/agent files, (3) third-party import paths/module names.

Applied to files:

  • src/synthorg/api/openapi.py
📚 Learning: 2026-03-19T07:09:59.660Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:09:59.660Z
Learning: Applies to src/**/*.py : 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. Tests must use test-provider, test-small-001, etc.

Applied to files:

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

612-616: Nice idempotent merge for info["x-documentation"].

This preserves pre-existing extension keys and only backfills rfc9457 when it is missing.

@Aureliolo Aureliolo merged commit 5d96b2b into main Mar 19, 2026
32 of 33 checks passed
@Aureliolo Aureliolo deleted the fix/openapi-schema-warnings branch March 19, 2026 07:56
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 19, 2026 07:56 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Mar 19, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.3.6](v0.3.5...v0.3.6)
(2026-03-19)


### Features

* **cli:** add backup subcommands (backup, backup list, backup restore)
([#568](#568))
([4c06b1d](4c06b1d))
* **engine:** implement execution loop auto-selection based on task
complexity ([#567](#567))
([5bfc2c6](5bfc2c6))


### Bug Fixes

* activate structured logging pipeline -- wire 8-sink system, integrate
Uvicorn, suppress spam
([#572](#572))
([9b6bf33](9b6bf33))
* **cli:** bump grpc-go v1.79.3 -- CVE-2026-33186 auth bypass
([#574](#574))
([f0171c9](f0171c9))
* resolve OpenAPI schema validation warnings for union/optional fields
([#558](#558))
([5d96b2b](5d96b2b))


### CI/CD

* bump codecov/codecov-action from 5.5.2 to 5.5.3 in the minor-and-patch
group ([#571](#571))
([267f685](267f685))
* ignore chainguard/python in Dependabot docker updates
([#575](#575))
([1935eaa](1935eaa))


### Maintenance

* bump the major group across 1 directory with 2 updates
([#570](#570))
([b98f82c](b98f82c))
* bump the minor-and-patch group across 2 directories with 4 updates
([#569](#569))
([3295168](3295168))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

fix: resolve OpenAPI schema validation warnings for union/optional fields

1 participant