Skip to content

Run Pylint 4 from lint target - #87

Merged
leynos merged 66 commits into
mainfrom
feat/pylint-v4-lint-target
May 14, 2026
Merged

Run Pylint 4 from lint target#87
leynos merged 66 commits into
mainfrom
feat/pylint-v4-lint-target

Conversation

@leynos

@leynos leynos commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

This branch adds a focused Pylint 4 pass to make lint and keeps that pass
usable on the managed PyPy runtime. It routes Pylint through
tools/pylint_pypy.py, stores the allow-listed message selection and rule rationale in
pyproject.toml, and updates
Makefile so lint, Ruff, architecture checks, and Pylint run as one gate.

The branch also flattens the PyPy Astroid object-builder compatibility patch,
removes dead helper code left by the refactor, restores strict test assertions
that should not be truthiness checks, and makes CI call
make lint directly.

Review walkthrough

Validation

  • mbake validate Makefile: passed
  • make check-fmt: passed
  • make markdownlint: passed
  • make nixie: passed
  • make lint: passed, Pylint reported 10.00/10
  • make typecheck: passed
  • make test: passed, 461 passed, 3 skipped

Notes

  • Pylint is configured as an allow-listed pass with disable = ["all"] and an explicit enable list for the requested Pylint 4 messages that are available in the installed 4.x release.
  • syntax-error remains disabled for this Pylint pass because the available managed PyPy is currently Python 3.11 while the project targets Python 3.14 syntax. The wrapper reports skipped parse-incompatible files instead of hiding other Pylint findings.
  • The requested rules that Pylint 4.0.5 does not recognise were not added to the enable list.

Summary by Sourcery

Add a PyPy-backed Pylint 4 lint gate and align codebase with the new focused rule set.

New Features:

  • Introduce a PyPy-driven Pylint 4 wrapper script and corresponding lint configuration with an allow-listed message set.
  • Wire the unified lint target to run architecture checks, Ruff, and Pylint together, and document the workflow for developers.

Enhancements:

  • Replace Ellipsis placeholders in protocol and port definitions with explicit NotImplementedError to clarify abstract contracts.
  • Tighten boolean and emptiness checks across validation, logging, and tests to align with new lint rules and improve clarity.
  • Adjust subprocess and assertion usage in tests to better express intent and comply with the updated lint configuration.
  • Add targeted tests for the PyPy-specific Astroid compatibility shim used by the Pylint wrapper.

CI:

  • Update CI workflow to run the consolidated lint gate via make lint instead of invoking individual tools directly.

Documentation:

  • Document the linting workflow, PyPy-backed Pylint usage, and rationale for the focused Pylint rule set in the developer guide.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

This PR integrates a PyPy-backed focused Pylint 4 pass into the repository lint gate and CI, supplies a PyPy/Astroid compatibility shim and wrapper, tightens and documents the Pylint allow-list, modernises typing to collections.abc across the codebase, and applies broad reorganisation, refactors and test consolidation. The unified lint target (make lint) now runs architecture checks, Ruff, then the PyPy-backed Pylint pass; CI also runs a CPython-only Pylint syntax check to surface Python 3.14-only syntax.

Summary of the most important changes

  • Linting infrastructure

    • Makefile: adds PYLINT_PYTHON (default pypy), PYLINT_TARGETS, a pinned pylint-pypy-shim ref, and a composed PYLINT command that runs the shim under the chosen Python. The lint target now runs Ruff then the PyPy-backed Pylint command against configured targets.
    • tools/pylint_pypy.py: new PyPy-aware Astroid wrapper/CLI that patches Astroid’s InspectBuilder.object_build to handle PyPy-specific behaviour (ignore non-string dir() entries, suppress certain getattr failures, unwrap bound methods except PyPy class_getitem, dispatch members to child builders, and attach dummy nodes for unresolved attributes). The module was refactored to extract/flatten helpers, remove dead helpers, tighten boolean/emptiness checks, add logging and module docstring, and includes targeted tests for the shim.
    • pyproject.toml: Pylint configured as a project-level allow-list (disable = ["all"] then explicit enables); scanning/design thresholds were added. The PyPy-backed Pylint run deliberately excludes Pylint’s syntax-error check because the managed PyPy runtime parses Python 3.11 while the project contains Python 3.14 syntax; files unparseable by the PyPy pass are reported and skipped so other diagnostics are not hidden.
    • docs/developers-guide.md: new “Linting” section documents make lint, the PyPy-backed Pylint invocation (uv tool run --python pypy with the pinned pylint-pypy-shim), the allow-list rationale, and the syntax-error caveat.
  • CI and validation

    • GitHub Actions: consolidated lint-test job to call make lint; added a separate CPython Pylint step that runs only syntax-error (pinned pylint==4.*) to catch 3.14-only syntax.
    • Validation reported in branch: mbake validate Makefile, check-fmt/markdownlint/nix, make lint (Pylint pass reported 10.00/10 for the new gate), make typecheck, and make test (461 passed, 3 skipped).
  • Tests, hygiene and observability

    • Many new and reorganised tests and shared test helpers were added across the codebase: focused PyPy shim tests, protocol-stub tests (asserting NotImplementedError), Pedante parsing/contract tests, show-notes parsing/generation/enrichment tests, health/lifespan tests, binding-resolution and reference-document integration tests, multi-source ingestion E2E tests, LangGraph property tests and many fixture/support modules.
    • Test-hygiene improvements: long tests and inline spies were refactored into helpers, bare assert statements received explanatory messages where appropriate, patched global state is restored for isolation, and new Hypothesis/property tests were requested/added for key invariants.
    • Observability: the PyPy shim adds logging for getattr failures and patch installation; module docstring documents invocation and responsibilities.
  • API, typing and module reorganisation

    • Canonical package decomposition: introduced protocol modules (entity_protocols.py, history_protocols.py, reference_protocols.py, unit_of_work_protocols.py). Several previously in-file protocol/mapper/model definitions were converted into compatibility re-export modules (for example episodic/canonical/ports.py, storage/models.py, storage/mappers.py) to preserve external import surfaces.
    • SQLAlchemy surface split: created models_base, entity_models, profile_models, history_models, reference_models and workflow_checkpoint_models; mapper modules (entity_mappers, history_mappers, reference_mappers) added with explicit all exports.
    • Import wiring updated across application and tests to reference the new modules; episodic/architecture/policy.py now includes the new canonical prefixes.
    • Typing modernisation: wide replacement of typing.Callable/Iterator/Sequence/AsyncIterator with collections.abc equivalents under TYPE_CHECKING; pyproject.toml includes flake8-tidy-imports banned-api guidance to encourage these patterns.
  • Complexity, protocol stubs and migrations

    • tools/pylint_pypy.py complexity reduced by extracting helpers, deleting dead code and simplifying guard logic; reviewers’ suggestions (move side-effects to callers, reduce compound conditions) were applied and limited noqa used where unavoidable.
    • Protocol stubs that previously used Ellipsis now raise NotImplementedError; tests/test_protocol_stubs.py verifies this behavior.
    • Alembic migration: created parameterised helpers for history-table DDL to reduce duplication while preserving explicit constraint/index names.
  • New/relocated functionality

    • LLM/OpenAI: added episodic/llm/openai_chat.py, openai_responses.py and openai_validation.py; episodic/llm/openai_client.py is now a compatibility façade re-exporting adapters and validation types.
    • Pedante: new episodic/qa/pedante package with typed DTOs, strict JSON parsing and PedanteEvaluator plus tests.
    • Orchestration DTOs/checkpoints: checkpoint/result DTOs reorganised into _checkpoint_dto and _result_dto; added WorkflowCheckpoint ORM model.
    • Tests: many new integration and unit tests across ingestion, orchestration, reference docs, show-notes, pedante, health endpoints and lifespans.

Review notes and recommended follow-ups

  • Primary review focus: tools/pylint_pypy.py complexity and test hygiene; iterative feedback produced helper extraction, dead-code removal, logging additions and improved test isolation—these changes are included in this branch.
  • Suggested follow-ups (some tracked as issues): add an end-to-end subprocess test for the shim CLI main() where CI supports PyPy; consider publishing the PyPy Astroid patch as a standalone package (pylint-pypy-shim) exposing a Pylint plugin/CLI with version/runtime guards and a smoke test; expand Hypothesis/property tests for dir() non-string filtering and getattr failure handling; resolve or suppress remaining CodeScene duplication diagnostics with explicit rationales.

Design/doc links

  • docs/developers-guide.md: new Linting section describing the unified make lint gate, the PyPy-backed Pylint pass via uv tool run --python pypy using the pinned pylint-pypy-shim, the allow-list approach and the rationale for excluding syntax-error from the PyPy pass.
  • episodic/architecture/policy.py: updated to reference the new canonical protocol module prefixes.

Execplan note

  • There is no new execplan document included in this PR to reference.

Walkthrough

Summarise: split canonical persistence into protocol and focused storage modules; add strict OpenAI payload validators/adapters; add Pedante evaluator and parsing; centralise ORM models/mappers; convert Protocol stubs to raise NotImplementedError; replace many typing generics with collections.abc; wire PyPy-run Pylint into Makefile/CI; add many shared test fixtures and new tests.

Changes

Canonical persistence: protocols & compatibility exports

Layer / File(s) Summary
Define focused repository protocols
episodic/canonical/entity_protocols.py, episodic/canonical/history_protocols.py, episodic/canonical/reference_protocols.py
Add typed async Protocol interfaces for entity, history and reference repositories (CRUD, listing, latest-revision helpers).
Unit-of-work protocol
episodic/canonical/unit_of_work_protocols.py
Add @runtime_checkable CanonicalUnitOfWork protocol declaring repository attributes and async context/transaction methods (stubs raise NotImplementedError).
Compatibility re-export surface
episodic/canonical/ports.py, episodic/canonical/__init__.py
Replace in-file protocol definitions with imports from the new *protocols modules and export them via __all__ for backwards compatibility.
Call-site type updates
multiple if TYPE_CHECKING: imports across episodic/* (examples: api/runtime.py, services.py, reference_documents/*)
Redirect type-only imports to the new unit_of_work_protocols and other *protocols modules to align runtime with new protocol locations.

ORM scaffolding and model decomposition

Layer / File(s) Summary
Shared ORM base and enums
episodic/canonical/storage/models_base.py
Introduce DeclarativeBase Base and central SQLAlchemy sa.Enum constants derived from domain enums.
Entity/profile/history/workflow models
episodic/canonical/storage/entity_models.py, .../profile_models.py, .../history_models.py, .../workflow_checkpoint_models.py
Add focused SQLAlchemy model modules for core entities, profile/templates, history tables, and workflow checkpoints.
Compatibility models façade
episodic/canonical/storage/models.py
Replace monolithic models file with a compatibility re-export module that imports focused model modules and defines an explicit __all__.

Mappers modularisation and re-exports

Layer / File(s) Summary
Dedicated mappers
episodic/canonical/storage/entity_mappers.py, .../history_mappers.py, .../reference_mappers.py
Add record↔domain mapper helpers for entities, history entries and reference artifacts, handling JSON/encoding/decoding and deep-copy semantics.
Compatibility mappers façade
episodic/canonical/storage/mappers.py
Replace prior in-file implementations with imports from the new mapper modules and export the original helper names via __all__.

Repository implementations & UoW wiring

Layer / File(s) Summary
Repository wiring updates
episodic/canonical/storage/repositories.py, .../history_repositories.py, .../reference_repositories.py
Point concrete repository implementations at the new protocol interfaces and mapper/model modules; update type annotations to use collections.abc where appropriate; add inline pylint disables on large signatures.
Unit-of-work implementation
episodic/canonical/storage/uow.py
Keep SqlAlchemyUnitOfWork behaviour but update type-only imports to reference unit_of_work_protocols.

OpenAI payload validation, adapters and client façade

Layer / File(s) Summary
Validation core
episodic/llm/openai_validation.py
Add OpenAIResponseValidationError and helpers to validate/normalise usage payloads and token fields for different OpenAI payload shapes.
Chat-completions adapter
episodic/llm/openai_chat.py
Implement strict chat-completions payload validators and OpenAIChatCompletionAdapter.normalize_chat_completion with explicit empty-first-choice handling.
Responses adapter
episodic/llm/openai_responses.py
Implement Responses-style payload validation and OpenAIResponsesAdapter.normalize_response.
Client façade
episodic/llm/openai_client.py
Turn prior module into a compatibility façade that re-exports the new adapters and validation symbols.
Protocol stubs
episodic/llm/ports.py, episodic/llm/openai_adapter.py
Tighten small guard checks and change some protocol method bodies to raise NotImplementedError.

Pedante evaluator, parsing and types

Layer / File(s) Summary
Pedante types and validators
episodic/qa/pedante/types.py
Add Pedante DTOs, enums and strict runtime validators for requests, findings and results; include usage and requires_revision semantics.
Strict JSON parsing
episodic/qa/pedante/parsing.py
Implement strict parser _evaluation_result_from_json that enforces field shapes and enum conversions, raising PedanteResponseFormatError on malformed output.
Evaluator entrypoint
episodic/qa/pedante/__init__.py
Add PedanteEvaluator dataclass with build_prompt and evaluate (calls LLMPort.generate and parses result), and export Pedante API via __all__.
Port stub change
episodic/qa/langgraph.py
Change evaluator port stub to raise NotImplementedError.

Orchestration DTOs and result split

Layer / File(s) Summary
Checkpoint DTOs
episodic/orchestration/_checkpoint_dto.py
Introduce immutable checkpoint DTOs, step identity and idempotency-key builder.
Result DTO
episodic/orchestration/_result_dto.py
Add GenerationOrchestrationResult dataclass with __post_init__ validation for action results.
Public DTO re-exports
episodic/orchestration/_dto.py
Replace inline checkpoint/result types with re-exports pointing to the new modules.
Typing hygiene
episodic/orchestration/checkpoints.py
Switch TimeProvider alias to use collections.abc.Callable.

Typing hygiene: switch typing generics → collections.abc

Layer / File(s) Summary
Mass type-only updates
many episodic/* and tests/* (examples: prompts.py, profile_templates/*, asyncio_tasks.py, prompts.py, many test fixtures)
Under TYPE_CHECKING import collections.abc as cabc and replace typing.* generic aliases with cabc.* equivalents; update casts and typ.cast targets accordingly across production and test code.

Protocol stub semantics & protocol-stub tests

Layer / File(s) Summary
Raise on protocol stubs
episodic/canonical/ingestion_ports.py, episodic/concurrent_interpreters.py, episodic/llm/ports.py
Replace ellipsis placeholders with raise NotImplementedError in several Protocol method bodies.
Unit test for stubs
tests/test_protocol_stubs.py
Add parametrised async test that constructs concrete subclasses of protocol stubs and asserts each declared method raises NotImplementedError.

Tooling, linting and CI

Layer / File(s) Summary
Makefile & lint wiring
Makefile
Add PYLINT_* variables, pin pylint-pypy-shim, and define PYLINT invocation that runs uv tool run --python $(PYLINT_PYTHON) with the shim; extend make lint to run the Pylint pass after Ruff.
Pylint/Ruff config & pytest timeout
pyproject.toml
Add Ruff banned-typing rules for typing.* generics and extensive Pylint configuration: disable-by-default (disable = ["all"]), curated enable list, design thresholds, and increase pytest kill timeout to 60s.
CI step
.github/workflows/ci.yml
Replace separate lint/architecture steps with make lint; add a focused Pylint syntax-only check step to validate CPython syntax using pinned pylint==4.*.
Developer docs
docs/developers-guide.md
Document the combined make lint gate and the PyPy-run Pylint shim approach and rationale.

Shared test fixtures, helpers and new tests

Layer / File(s) Summary
Shared test utilities
tests/api_fixtures.py, tests/workflow_test_utils.py, tests/show_notes_support.py, tests/fixtures/*, tests/steps/*
Add many reusable test fixtures and helpers (API fixture builders, act runner/artifact reader, FakeLLM port, async task-factory recorder, profile/template fixtures, VidaiMock helpers, ingestion integration helpers, binding-resolution helpers).
Refactor tests to use shared helpers
many tests/* (examples: test_reference_documents, test_ingestion_integration, test_generation_orchestration_steps)
Replace prior in-file helper implementations with imports from the new shared modules and update call sites accordingly.
New unit and integration tests
tests/* (examples: test_health_endpoints.py, test_env_runtime_wiring.py, test_ingestion_integration_e2e.py, test_pedante_evaluator_integration.py, test_show_notes_*, test_orchestration_*, canonical_storage/test_reference_document_bindings.py)
Add broad coverage: Pedante evaluator, OpenAI adapters, ingestion E2E, orchestration properties, health/env wiring, reference-binding repository constraints, show-notes parsing/generation/enrichment and many supporting unit tests.

Small behaviour-preserving cleanups

Layer / File(s) Summary
Micro cleanups and lint refinements
episodic/benchmarks/interpreters.py, episodic/llm/openai_adapter.py, various tests/*, episodic/api/resources/reference_documents.py
Change trivial guard checks (if combined == ""if not combined, candidate % divisor check using truthiness), refine inline lint suppressions, and add pylint disable-next directives on Falcon handlers.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(0,128,255,0.5)
    participant Client
    end
    rect rgba(0,200,100,0.5)
    participant App
    end
    rect rgba(255,128,0,0.5)
    participant LLM
    end
    rect rgba(200,0,200,0.5)
    participant DB
    end

    Client->>App: POST /series-profiles/{id}/pedante (PedanteEvaluationRequest)
    App->>App: PedanteEvaluator.build_prompt(request)
    App->>LLM: LLMPort.generate(LLMRequest with prompt)
    LLM-->>App: LLMResponse (text + usage + metadata)
    App->>App: PedanteEvaluationResult.from_json(response.text, usage)
    App->>DB: Optional reads/writes via SqlAlchemyUnitOfWork
    DB-->>App: Query results / commit
    App-->>Client: 200 OK with typed PedanteEvaluationResult
Loading

Possibly related PRs

"Poem"

Lint under PyPy, protocols split and shorn,
Models moved, mappers reborn.
Validators guard the LLM gate,
Tests converge and CI update.
Run the suite; land the changes; celebrate.

✨ 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 feat/pylint-v4-lint-target

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 11, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +27 to +80

def _object_build_without_pypy_descriptor_aliases(
    self: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    obj: types.ModuleType | type,
) -> None:
    """Build Astroid nodes while ignoring non-string PyPy ``dir()`` entries."""
    if obj in self._done:
        return
    self._done[obj] = node
    for alias in dir(obj):
        if type(alias) is not str:
            continue
        pypy__class_getitem__ = IS_PYPY and alias == "__class_getitem__"
        try:
            with warnings.catch_warnings():
                warnings.simplefilter("ignore")
                member = getattr(obj, alias)
        except _IGNORED_GETATTR_ERRORS:
            attach_dummy_node(node, alias)
            continue
        if inspect.ismethod(member) and not pypy__class_getitem__:
            member = member.__func__
        if inspect.isfunction(member):
            child = _build_from_function(node, member, self._module)
        elif inspect.isbuiltin(member) or pypy__class_getitem__:
            if self.imported_member(node, member, alias):
                continue
            child = object_build_methoddescriptor(node, member)
        elif inspect.isclass(member):
            if self.imported_member(node, member, alias):
                continue
            if member in self._done:
                child = self._done[member]
                assert isinstance(child, nodes.ClassDef)
            else:
                child = object_build_class(node, member)
                self.object_build(child, member)
        elif inspect.ismethoddescriptor(member):
            child = object_build_methoddescriptor(node, member)
        elif inspect.isdatadescriptor(member):
            child = object_build_datadescriptor(node, member)
        elif isinstance(member, tuple(node_classes.CONST_CLS)):
            if alias in node.special_attributes:
                continue
            child = nodes.const_factory(member)
        elif inspect.isroutine(member):
            child = _build_from_function(node, member, self._module)
        elif _safe_has_attribute(member, "__all__"):
            child = build_module(alias)
            self.object_build(child, member)
        else:
            child = build_dummy(member)
        if child not in node.locals.get(alias, ()):
            node.add_local_node(child, alias)

❌ New issue: Complex Method
_object_build_without_pypy_descriptor_aliases has a cyclomatic complexity of 24, threshold = 9

@leynos

leynos commented May 11, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +27 to +80

def _object_build_without_pypy_descriptor_aliases(
    self: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    obj: types.ModuleType | type,
) -> None:
    """Build Astroid nodes while ignoring non-string PyPy ``dir()`` entries."""
    if obj in self._done:
        return
    self._done[obj] = node
    for alias in dir(obj):
        if type(alias) is not str:
            continue
        pypy__class_getitem__ = IS_PYPY and alias == "__class_getitem__"
        try:
            with warnings.catch_warnings():
                warnings.simplefilter("ignore")
                member = getattr(obj, alias)
        except _IGNORED_GETATTR_ERRORS:
            attach_dummy_node(node, alias)
            continue
        if inspect.ismethod(member) and not pypy__class_getitem__:
            member = member.__func__
        if inspect.isfunction(member):
            child = _build_from_function(node, member, self._module)
        elif inspect.isbuiltin(member) or pypy__class_getitem__:
            if self.imported_member(node, member, alias):
                continue
            child = object_build_methoddescriptor(node, member)
        elif inspect.isclass(member):
            if self.imported_member(node, member, alias):
                continue
            if member in self._done:
                child = self._done[member]
                assert isinstance(child, nodes.ClassDef)
            else:
                child = object_build_class(node, member)
                self.object_build(child, member)
        elif inspect.ismethoddescriptor(member):
            child = object_build_methoddescriptor(node, member)
        elif inspect.isdatadescriptor(member):
            child = object_build_datadescriptor(node, member)
        elif isinstance(member, tuple(node_classes.CONST_CLS)):
            if alias in node.special_attributes:
                continue
            child = nodes.const_factory(member)
        elif inspect.isroutine(member):
            child = _build_from_function(node, member, self._module)
        elif _safe_has_attribute(member, "__all__"):
            child = build_module(alias)
            self.object_build(child, member)
        else:
            child = build_dummy(member)
        if child not in node.locals.get(alias, ()):
            node.add_local_node(child, alias)

❌ New issue: Bumpy Road Ahead
_object_build_without_pypy_descriptor_aliases has 4 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 11, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +83 to +109

def _build_child_node(  # noqa: PLR0913
    self: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    member: object,
    alias: str,
    *,
    pypy__class_getitem__: bool,
) -> nodes.NodeNG | object:
    """Dispatch member conversion to the matching Astroid child builder."""
    if inspect.isbuiltin(member) or pypy__class_getitem__:
        child = _build_builtin_child(self, node, member, alias)
    elif inspect.isclass(member):
        child = _build_class_child(self, node, member, alias)
    elif inspect.ismethoddescriptor(member):
        child = object_build_methoddescriptor(node, member)
    elif inspect.isdatadescriptor(member):
        child = object_build_datadescriptor(node, member)
    elif isinstance(member, tuple(node_classes.CONST_CLS)):
        child = _build_const_child(node, member, alias)
    elif inspect.isfunction(member) or inspect.isroutine(member):
        child = _build_from_function(node, member, self._module)
    elif _safe_has_attribute(member, "__all__"):
        child = build_module(alias)
        self.object_build(child, member)
    else:
        child = build_dummy(member)
    return child

❌ New issue: Complex Method
_build_child_node has a cyclomatic complexity of 11, threshold = 9

@leynos

leynos commented May 11, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +112 to +141

def _object_build_without_pypy_descriptor_aliases(
    self: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    obj: types.ModuleType | type,
) -> None:
    """Build Astroid nodes while ignoring non-string PyPy ``dir()`` entries."""
    if obj in self._done:
        return
    self._done[obj] = node
    for alias in dir(obj):
        if type(alias) is not str:
            continue
        pypy__class_getitem__ = IS_PYPY and alias == "__class_getitem__"
        member = _get_member(obj, alias)
        if member is _GET_MEMBER_FAILED:
            attach_dummy_node(node, alias)
            continue
        if inspect.ismethod(member) and not pypy__class_getitem__:
            member = member.__func__
        child = _build_child_node(
            self,
            node,
            member,
            alias,
            pypy__class_getitem__=pypy__class_getitem__,
        )
        if child is _SKIP:
            continue
        if child not in node.locals.get(alias, ()):
            node.add_local_node(child, alias)

❌ New issue: Complex Method
_object_build_without_pypy_descriptor_aliases has a cyclomatic complexity of 10, threshold = 9

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 11, 2026

Copy link
Copy Markdown
Owner Author

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

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

tools/pylint_pypy.py

Comment on lines +87 to +110

def _dispatch_member_to_child(
    self: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    member: object,
    alias: str,
) -> nodes.NodeNG | object:
    """Dispatch non-PyPy-special members to the matching Astroid builder."""
    if inspect.isbuiltin(member):
        return _build_builtin_child(self, node, member, alias)
    if inspect.isclass(member):
        return _build_class_child(self, node, member, alias)
    if inspect.ismethoddescriptor(member):
        return object_build_methoddescriptor(node, member)
    if inspect.isdatadescriptor(member):
        return object_build_datadescriptor(node, member)
    if isinstance(member, tuple(node_classes.CONST_CLS)):
        return _build_const_child(node, member, alias)
    if inspect.isroutine(member):
        return _build_from_function(node, member, self._module)
    if _safe_has_attribute(member, "__all__"):
        child = build_module(alias)
        self.object_build(child, member)
        return child
    return build_dummy(member)

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: _build_child_node,_dispatch_member_to_child

@leynos

leynos commented May 11, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +139 to +162

def _object_build_without_pypy_descriptor_aliases(
    self: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    obj: types.ModuleType | type,
) -> None:
    """Build Astroid nodes while ignoring non-string PyPy ``dir()`` entries."""
    if obj in self._done:
        return
    self._done[obj] = node
    for alias in dir(obj):
        if type(alias) is not str:
            continue
        member, pypy__class_getitem__ = _get_member(obj, alias)
        if member is _GET_MEMBER_FAILED:
            attach_dummy_node(node, alias)
            continue
        if pypy__class_getitem__:
            child = _build_builtin_child(self, node, member, alias)
        else:
            child = _build_child_node(self, node, member, alias)
        if child is _SKIP:
            continue
        if child not in node.locals.get(alias, ()):
            node.add_local_node(child, alias)

❌ New issue: Complex Method
_object_build_without_pypy_descriptor_aliases has a cyclomatic complexity of 9, threshold = 9

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 12, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +97 to +124

def _dispatch_member_to_child(  # noqa: C901, PLR0913
    self: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    member: object,
    alias: str,
    *,
    pypy__class_getitem__: bool = False,
) -> nodes.NodeNG | object:
    """Dispatch members to the matching Astroid builder."""
    if pypy__class_getitem__:
        return _build_builtin_child(self, node, member, alias)
    if inspect.isbuiltin(member):
        return _build_builtin_child(self, node, member, alias)
    if inspect.isclass(member):
        return _build_class_child(self, node, member, alias)
    if inspect.ismethoddescriptor(member):
        return object_build_methoddescriptor(node, member)
    if inspect.isdatadescriptor(member):
        return object_build_datadescriptor(node, member)
    if isinstance(member, tuple(node_classes.CONST_CLS)):
        return _build_const_child(node, member, alias)
    if inspect.isroutine(member):
        return _build_from_function(node, member, self._module)
    if _safe_has_attribute(member, "__all__"):
        child = build_module(alias)
        self.object_build(child, member)
        return child
    return build_dummy(member)

❌ New issue: Complex Method
_dispatch_member_to_child has a cyclomatic complexity of 9, threshold = 9

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 12, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +146 to +184

def _build_child_for_member(  # noqa: PLR0913, PLR0917
    builder: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    member: object,
    alias: str,
    pypy__class_getitem__: bool,  # noqa: FBT001
) -> nodes.NodeNG | None:
    """Build an Astroid child for *member* or return None when it should skip."""
    if inspect.isfunction(member):
        return _build_from_function(node, member, builder._module)
    if inspect.isbuiltin(member) or pypy__class_getitem__:
        if builder.imported_member(node, member, alias):
            return None
        return object_build_methoddescriptor(node, member)
    if inspect.isclass(member):
        if builder.imported_member(node, member, alias):
            return None
        if member in builder._done:
            child = builder._done[member]
            assert isinstance(child, nodes.ClassDef)
            return child
        child = object_build_class(node, member)
        builder.object_build(child, member)
        return child
    if inspect.ismethoddescriptor(member):
        return object_build_methoddescriptor(node, member)
    if inspect.isdatadescriptor(member):
        return object_build_datadescriptor(node, member)
    if isinstance(member, tuple(node_classes.CONST_CLS)):
        if alias in node.special_attributes:
            return None
        return nodes.const_factory(member)
    if inspect.isroutine(member):
        return _build_from_function(node, member, builder._module)
    if _safe_has_attribute(member, "__all__"):
        child = build_module(alias)
        builder.object_build(child, member)
        return child
    return build_dummy(member)

❌ New issue: Excess Number of Function Arguments
_build_child_for_member has 5 arguments, max arguments = 4

@leynos

leynos commented May 12, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on file

# ruff: noqa: C901, PLR0911, S101, TC003

❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 5.00 across 10 functions. The mean complexity threshold is 4

@leynos

leynos commented May 12, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +146 to +184

def _build_child_for_member(  # noqa: PLR0913, PLR0917
    builder: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    member: object,
    alias: str,
    pypy__class_getitem__: bool,  # noqa: FBT001
) -> nodes.NodeNG | None:
    """Build an Astroid child for *member* or return None when it should skip."""
    if inspect.isfunction(member):
        return _build_from_function(node, member, builder._module)
    if inspect.isbuiltin(member) or pypy__class_getitem__:
        if builder.imported_member(node, member, alias):
            return None
        return object_build_methoddescriptor(node, member)
    if inspect.isclass(member):
        if builder.imported_member(node, member, alias):
            return None
        if member in builder._done:
            child = builder._done[member]
            assert isinstance(child, nodes.ClassDef)
            return child
        child = object_build_class(node, member)
        builder.object_build(child, member)
        return child
    if inspect.ismethoddescriptor(member):
        return object_build_methoddescriptor(node, member)
    if inspect.isdatadescriptor(member):
        return object_build_datadescriptor(node, member)
    if isinstance(member, tuple(node_classes.CONST_CLS)):
        if alias in node.special_attributes:
            return None
        return nodes.const_factory(member)
    if inspect.isroutine(member):
        return _build_from_function(node, member, builder._module)
    if _safe_has_attribute(member, "__all__"):
        child = build_module(alias)
        builder.object_build(child, member)
        return child
    return build_dummy(member)

❌ New issue: Bumpy Road Ahead
_build_child_for_member has 3 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

@leynos

leynos commented May 12, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +97 to +124

def _dispatch_member_to_child(  # noqa: PLR0913
    self: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    member: object,
    alias: str,
    *,
    pypy__class_getitem__: bool = False,
) -> nodes.NodeNG | object:
    """Dispatch members to the matching Astroid builder."""
    if pypy__class_getitem__:
        return _build_builtin_child(self, node, member, alias)
    if inspect.isbuiltin(member):
        return _build_builtin_child(self, node, member, alias)
    if inspect.isclass(member):
        return _build_class_child(self, node, member, alias)
    if inspect.ismethoddescriptor(member):
        return object_build_methoddescriptor(node, member)
    if inspect.isdatadescriptor(member):
        return object_build_datadescriptor(node, member)
    if isinstance(member, tuple(node_classes.CONST_CLS)):
        return _build_const_child(node, member, alias)
    if inspect.isroutine(member):
        return _build_from_function(node, member, self._module)
    if _safe_has_attribute(member, "__all__"):
        child = build_module(alias)
        self.object_build(child, member)
        return child
    return build_dummy(member)

❌ New issue: Complex Method
_dispatch_member_to_child has a cyclomatic complexity of 9, threshold = 9

1 similar comment
@leynos

leynos commented May 12, 2026

Copy link
Copy Markdown
Owner Author

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

tools/pylint_pypy.py

Comment on lines +97 to +124

def _dispatch_member_to_child(  # noqa: PLR0913
    self: raw_building.InspectBuilder,
    node: nodes.Module | nodes.ClassDef,
    member: object,
    alias: str,
    *,
    pypy__class_getitem__: bool = False,
) -> nodes.NodeNG | object:
    """Dispatch members to the matching Astroid builder."""
    if pypy__class_getitem__:
        return _build_builtin_child(self, node, member, alias)
    if inspect.isbuiltin(member):
        return _build_builtin_child(self, node, member, alias)
    if inspect.isclass(member):
        return _build_class_child(self, node, member, alias)
    if inspect.ismethoddescriptor(member):
        return object_build_methoddescriptor(node, member)
    if inspect.isdatadescriptor(member):
        return object_build_datadescriptor(node, member)
    if isinstance(member, tuple(node_classes.CONST_CLS)):
        return _build_const_child(node, member, alias)
    if inspect.isroutine(member):
        return _build_from_function(node, member, self._module)
    if _safe_has_attribute(member, "__all__"):
        child = build_module(alias)
        self.object_build(child, member)
        return child
    return build_dummy(member)

❌ New issue: Complex Method
_dispatch_member_to_child has a cyclomatic complexity of 9, threshold = 9

@leynos

leynos commented May 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Large Method

tests/test_pedante_evaluator_integration.py: test_pedante_evaluator_returns_typed_findings_and_usage

What lead to degradation?

test_pedante_evaluator_returns_typed_findings_and_usage has 86 lines, threshold = 70

Why does this problem occur?

Overly long functions make the code harder to read. The recommended maximum function length for the Python language is 70 lines of code. Severity: Brain Method - Complex Method - Long Method.

How to fix it?

We recommend to be careful here -- just splitting long functions don't necessarily make the code easier to read. Instead, look for natural chunks inside the functions that expresses a specific task or concern. Often, such concerns are indicated by a Code Comment followed by an if-statement. Use the EXTRACT FUNCTION refactoring to encapsulate that concern.

@leynos

leynos commented May 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Large Method

tests/test_orchestration_langgraph_properties.py: test_langgraph_total_tokens_non_negative

What lead to degradation?

test_langgraph_total_tokens_non_negative has 75 lines, threshold = 70

Why does this problem occur?

Overly long functions make the code harder to read. The recommended maximum function length for the Python language is 70 lines of code. Severity: Brain Method - Complex Method - Long Method.

How to fix it?

We recommend to be careful here -- just splitting long functions don't necessarily make the code easier to read. Instead, look for natural chunks inside the functions that expresses a specific task or concern. Often, such concerns are indicated by a Code Comment followed by an if-statement. Use the EXTRACT FUNCTION refactoring to encapsulate that concern.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

Move evaluator setup and request/result assertions out of the integration
scenario so the test body stays focused on the payload and invocation. Keep
existing typed finding and provider metadata checks intact.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Move planner/action construction, graph execution, and usage rollup checks
out of the Hypothesis property body. Keep the property focused on the
orchestration flow while preserving the token and result assertions.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Expand OpenAI chat and Pedante DTO documentation where review requested
public contract details. Add runtime validation for Pedante enum fields,
result findings, and usage metadata so malformed DTOs fail at construction.

Refactor show-notes generation tests to share payload fixtures and keep
assertion failures diagnostic.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
episodic/qa/pedante/types.py (1)

102-106: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject None for usage and enforce the declared contract.

Tighten this guard. Line 104 currently admits None, even though PedanteEvaluationResult.usage is typed as LLMUsage, which weakens contract safety and defers failures.

Patch
 def _require_usage(value: object) -> None:
     """Reject usage metadata that is not normalized LLM usage."""
-    if value is not None and not isinstance(value, LLMUsage):
+    if not isinstance(value, LLMUsage):
         msg = "usage must be an LLMUsage value."
         raise TypeError(msg)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@episodic/qa/pedante/types.py` around lines 102 - 106, The guard in
_require_usage currently allows None which contradicts the declared
PedanteEvaluationResult.usage: LLMUsage; update _require_usage so it rejects
None as well as non-LLMUsage types — i.e., treat value is None as an error and
raise a TypeError with a clear message; locate the function _require_usage and
change its validation to require isinstance(value, LLMUsage) (no None allowed)
so callers and the PedanteEvaluationResult.usage contract are enforced.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@episodic/qa/pedante/types.py`:
- Around line 102-106: The guard in _require_usage currently allows None which
contradicts the declared PedanteEvaluationResult.usage: LLMUsage; update
_require_usage so it rejects None as well as non-LLMUsage types — i.e., treat
value is None as an error and raise a TypeError with a clear message; locate the
function _require_usage and change its validation to require isinstance(value,
LLMUsage) (no None allowed) so callers and the PedanteEvaluationResult.usage
contract are enforced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7e1e08d4-7a4c-4100-8615-6085d28b03b3

📥 Commits

Reviewing files that changed from the base of the PR and between e82eef7 and d82dc6b.

📒 Files selected for processing (4)
  • episodic/llm/openai_chat.py
  • episodic/qa/pedante/types.py
  • tests/test_pedante_validation.py
  • tests/test_show_notes_generation.py

@leynos

leynos commented May 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Large Method

tests/test_orchestration_langgraph_properties.py: test_langgraph_total_tokens_non_negative

What lead to degradation?

test_langgraph_total_tokens_non_negative has 75 lines, threshold = 70

Why does this problem occur?

Overly long functions make the code harder to read. The recommended maximum function length for the Python language is 70 lines of code. Severity: Brain Method - Complex Method - Long Method.

How to fix it?

We recommend to be careful here -- just splitting long functions don't necessarily make the code easier to read. Instead, look for natural chunks inside the functions that expresses a specific task or concern. Often, such concerns are indicated by a Code Comment followed by an if-statement. Use the EXTRACT FUNCTION refactoring to encapsulate that concern.

@coderabbitai

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant