Skip to content

Bring oversized files under the 400-line guideline (#108) - #136

Merged
leynos merged 4 commits into
mainfrom
issue-108-bring-oversized-files-under-400-lines
Jul 9, 2026
Merged

Bring oversized files under the 400-line guideline (#108)#136
leynos merged 4 commits into
mainfrom
issue-108-bring-oversized-files-under-400-lines

Conversation

@leynos

@leynos leynos commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #108

Note: this branch merges the #95 (issue-95-canonical-bump-output, PR #122), #96 (PR #123), and #97 (PR #121) branches, as the issue prescribes landing the duplication removals first. Merging those PRs before this one keeps the diff here to the extraction work.

  • lading/toml_coerce.py — new canonical home for the TOML scalar/sequence/mapping coercion helpers previously duplicated by config.py and workspace/models.py. Helpers take an error keyword naming the LadingError subclass to raise; both consumers bind their domain error type once (functools.partial, or a typed wrapper where overload narrowing must survive). One canonical message shape: {field} must be {expected}; received {type}.
  • workspace/graph_build.py — cargo-metadata→model builders out of models.py (which keeps the structures and topology).
  • commands/publish_pipeline.py — per-crate package/publish pipeline, result classification, and mode dispatch out of publish.py (re-split after merging main's cargo-output-adapter rework from Move cargo output parsing to an adapter (#69) #88).
  • commands/bump_manifests.py — per-manifest rewriting out of bump.py.
  • cli_options.py — Cyclopts argument declarations and version-argument validation out of cli.py; bump_docs.py condenses a duplicative module docstring.

Every extraction is re-exported from its original home so existing import paths and test access keep resolving. All source files are now ≤398 lines. The developers' guide documents the coercion module, its error convention, and the extraction map.

Testing

  • tests/unit/test_toml_coerce.py: Hypothesis property tests for each coercion helper (well-typed pass-through; ill-typed raise with the canonical message including type(x).__name__), plus a syrupy snapshot of representative messages.
  • make check-fmt, make lint, make typecheck, and make test (594 passed) all green after merging current main.
  • coderabbit review --agent: 0 findings.

References

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 36 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e74209c4-001f-4702-b038-e9613bf1cec6

📥 Commits

Reviewing files that changed from the base of the PR and between e53d6e1 and c95c4a9.

📒 Files selected for processing (6)
  • lading/commands/bump_manifests.py
  • lading/toml_coerce/_scalars.py
  • lading/toml_coerce/_sequences.py
  • lading/workspace/graph_build.py
  • tests/unit/__snapshots__/test_toml_coerce.ambr
  • tests/unit/test_toml_coerce.py

Walkthrough

The PR extracts oversized modules into dedicated components: shared TOML coercion helpers (lading.toml_coerce), bump manifest logic (bump_manifests), workspace graph construction (graph_build, _coercion), and CLI option declarations (cli_options). Existing modules delegate to the new components, with tests, snapshots, and documentation updated accordingly.

Changes

Module extraction and wiring

Layer / File(s) Summary
Shared TOML coercion module
lading/toml_coerce/*, lading/config.py, docs/developers-guide.md, tests/unit/test_toml_coerce.py, tests/unit/__snapshots__/test_toml_coerce.ambr
Adds canonical scalar/mapping/sequence coercion helpers with a shared error-message shape; config.py binds them via functools.partial; property tests and a stable error-message snapshot are added.
Bump manifest extraction
lading/commands/bump.py, lading/commands/bump_manifests.py, lading/commands/bump_docs.py, tests/unit/test_dependency_sections.py, docs/developers-guide.md, docs/users-guide.md
Per-crate manifest rewriting, selector derivation, and dependency-section grouping move to bump_manifests; bump.py now consumes bump_manifests._BumpContext directly; docs and tests updated to reference the new module.
Workspace graph extraction
lading/workspace/graph_build.py, lading/workspace/_coercion.py, lading/workspace/models.py, lading/workspace/__init__.py, tests/unit/test_workspace_models_validation.py
Workspace-graph construction (load_workspace, build_workspace_graph) and validation helpers move from models.py into graph_build.py, backed by a new _coercion.py module binding shared coercers to WorkspaceModelError; tests redirected accordingly.
CLI option extraction and version validation move
lading/cli.py, lading/cli_options.py, tests/unit/test_cli.py, tests/bdd/steps/test_bump_steps.py
Cyclopts option/flag declarations and semantic-version validation move from cli.py into cli_options.py; tests updated to expect Cyclopts validation errors on stdout rather than stderr.

Sequence Diagram(s)

sequenceDiagram
  participant load_workspace
  participant build_workspace_graph
  participant cargo_metadata
  participant CargoToml
  load_workspace->>cargo_metadata: load_cargo_metadata()
  cargo_metadata-->>build_workspace_graph: metadata
  build_workspace_graph->>CargoToml: inspect package.readme.workspace
  CargoToml-->>build_workspace_graph: manifest data
  build_workspace_graph-->>load_workspace: WorkspaceGraph
Loading

Possibly related PRs

  • leynos/lading#85: Both touch the --rebuild-lockfiles/--no-rebuild-lockfiles CLI surface, defining/centralising the RebuildLockfilesFlag option.
  • leynos/lading#90: Both touch --allow-unpublished-workspace-deps plumbing, with this PR centralising the tri-state declaration/validation into cli_options.py.
  • leynos/lading#114: Both refactor Cargo dependency-table section handling sourced from bump_toml.DEPENDENCY_SECTIONS.

Suggested labels: Issue

Suggested reviewers: codescene-delta-analysis, codescene-access

Poem

A rabbit hopped through modules vast,
Splitting files grown far too fast,
Coercion, bump, and workspace graph,
Each found a home, tidy and safe.
Cyclopts flags now stand alone —
Four hundred lines, a happy zone! 🐇✂️


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Most new paths are tested, but the TOML coercion suite leaves canonical-message branches unguarded: expect_sequence(None) and string_matrix row failures only assert raises, not wording. Assert the exact error text for the disallowed-None branch and row-rejection branches, and add a valid expect_mapping success case so the canonical contract is locked down.
Module-Level Documentation ❌ Error graph_build.py’s docstring still says the coercion bindings are defined in models, but they live in lading.workspace._coercion. Update the graph_build module docstring to point at lading.workspace._coercion and keep the module relationship wording accurate.
Architectural Complexity And Maintainability ⚠️ Warning bump.py remains 427 lines and publish.py 741, with no documented exception, so the extraction has not materially reduced the oversized orchestration burden. Split or trim the remaining oversized orchestrators, or add a documented exception with clear ownership and rationale for each file above 400 lines.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed Align the title with the change set and keep the required #108 reference.
Description check ✅ Passed Keep the description tied to the extraction work and #108.
Linked Issues check ✅ Passed Implement the shared coercion extraction, docs, and tests required by #108.
Out of Scope Changes check ✅ Passed Keep the added modules, docs, and tests within the stated scope for #108.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
User-Facing Documentation ✅ Passed PASS: docs/users-guide.md documents the changed CLI and bump/publish behaviour, including --allow-unpublished-workspace-deps, --live sequencing, lockfiles, and README adoption.
Developer Documentation ✅ Passed docs/developers-guide.md now documents the shared coercion home and issue-108 module extractions; no execplan or roadmap item needed.
Testing (Unit And Behavioural) ✅ Passed PASS: Property/snapshot unit tests cover toml_coerce edge cases; BDD and e2e scenarios exercise bump/publish through the CLI subprocess boundary.
Testing (Property / Proof) ✅ Passed Accept this: tests/unit/test_toml_coerce.py uses Hypothesis for each coercer and snapshots representative failures; the guide documents the property-testing pattern.
Testing (Compile-Time / Ui) ✅ Passed No Rust/TypeScript compile-time surface exists, so trybuild is N/A; the PR adds focused Hypothesis coverage plus a syrupy snapshot for canonical TOML coercion messages.
Unit Architecture ✅ Passed PASS: Cargo subprocesses, file writes, and filesystem parsing stay behind explicit loaders/builders; command modules inject runners/options rather than reaching for hidden globals.
Domain Architecture ✅ Passed PASS: domain concerns are separated; models now hold only workspace types/topology, while cargo metadata, TOML coercion, and CLI/env parsing live in adapter modules, with docs updated.
Observability ✅ Passed PASS: This is extraction-only; existing decision-point logs remain in cli/bump/bump_docs, and no new metrics, tracing, or alerts were added.
Security And Privacy ✅ Passed PASS: The PR only adds local CLI flags, validation, and docs; live-mode relaxation is explicitly rejected, and no secrets or credentials appear in code, tests, or docs.
Performance And Resource Use ✅ Passed PASS: The extracted code preserves the prior linear scans and one-read-per-manifest behaviour; no new nested walks, retries, or unbounded buffers appeared.
Concurrency And State ✅ Passed PASS: extracted helpers are pure or frozen dataclasses; no async, locks, background tasks, or shared mutable state were introduced, and dependency sections are frozen with MappingProxyType.
Rust Compiler Lint Integrity ✅ Passed No Rust sources were touched; the diff is confined to Python, docs, and tests, so this lint-integrity check is not applicable.
📋 Issue Planner

Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).

View plan for ticket: #108

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-108-bring-oversized-files-under-400-lines

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

@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

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Jun 10, 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.

lading/commands/publish_pipeline.py

Comment on lines +109 to +123

def _package_publishable_crates(
    plan: PublishPlan,
    preparation: PublishPreparation,
    *,
    options: _PublishExecutionOptions,
    runner: CommandRunner,
) -> None:
    """Package each publishable crate in order using the staged workspace."""
    state = _PublicationPipelineState(plan, preparation, options)
    for crate in plan.publishable:
        _package_crate(
            crate,
            state,
            runner=runner,
        )

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: _package_publishable_crates,_publish_crates

@leynos

leynos commented Jun 10, 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.

lading/toml_coerce.py

Comment on file

"""Shared TOML scalar, sequence, and mapping coercion helpers.

❌ New issue: Global Conditionals
There's global code outside of functions with a cyclomatic complexity of 32, threshold = 10

@leynos

leynos commented Jun 10, 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.

tests/unit/test_bump_command_internals.py

Comment on file

        changes, "1.2.3", dry_run=False, workspace_root=root
    )

    if not (manifests or documents or readmes or lockfiles):

❌ New issue: Complex Conditional
test_result_message_grammar_and_path_rendering has 1 complex conditionals with 3 branches, threshold = 2

@leynos

leynos commented Jun 10, 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.

tests/unit/test_bump_command_internals.py

Comment on file

import dataclasses as dc
import typing as typ
from pathlib import Path

❌ New issue: Low Cohesion
This module has at least 27 different responsibilities amongst its 35 functions, threshold = 4

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@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 Jun 10, 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.

tests/unit/test_bump_context_and_output.py

Comment on lines +216 to +267

def test_result_message_grammar_and_path_rendering(
    manifest_count: int,
    document_count: int,
    readme_count: int,
    lockfile_count: int,
) -> None:
    """Any category combination renders correct grammar and unique paths."""
    root = Path("/ws")
    manifests = tuple(root / f"m{i}" / "Cargo.toml" for i in range(manifest_count))
    documents = tuple(root / f"doc{i}.md" for i in range(document_count))
    readmes = tuple(root / f"r{i}" / "README.md" for i in range(readme_count))
    lockfiles = tuple(root / f"l{i}" / "Cargo.lock" for i in range(lockfile_count))
    changes = bump_output.BumpChanges(
        manifests=manifests,
        documents=documents,
        lockfiles=lockfiles,
        transposed_readmes=readmes,
    )

    message = bump_output._format_result_message(
        changes, "1.2.3", dry_run=False, workspace_root=root
    )

    if not (manifests or documents or readmes or lockfiles):
        assert message == "No manifest changes required; all versions already 1.2.3."
        return

    lines = message.splitlines()
    expected_body = [
        *(f"- {path.relative_to(root)}" for path in manifests),
        *(f"- {path.relative_to(root)} (documentation)" for path in documents),
        *(f"- {path.relative_to(root)} (readme)" for path in readmes),
        *(f"- {path.relative_to(root)} (lockfile)" for path in lockfiles),
    ]
    assert lines[1:] == expected_body

    categories = []
    if manifests:
        categories.append(f"{len(manifests)} manifest(s)")
    if documents:
        categories.append(f"{len(documents)} documentation file(s)")
    if readmes:
        categories.append(f"{len(readmes)} readme file(s)")
    if lockfiles:
        categories.append(f"{len(lockfiles)} lockfile(s)")
    expected_header = (
        f"Updated version to 1.2.3 in {_expected_description(categories)}:"
    )
    assert lines[0] == expected_header
    assert " and and " not in lines[0]
    if len(categories) >= 3:
        assert ", and " in lines[0]

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

@coderabbitai

This comment was marked as resolved.

@lodyai
lodyai Bot force-pushed the issue-108-bring-oversized-files-under-400-lines branch from 7da9557 to 22b4f4e Compare June 10, 2026 22:22
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Jun 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.

Complex Conditional

tests/unit/test_bump_context_and_output.py: test_result_message_grammar_and_path_rendering

What lead to degradation?

test_result_message_grammar_and_path_rendering has 1 complex conditionals with 3 branches, threshold = 2

Why does this problem occur?

A complex conditional is an expression inside a branch such as an if-statmeent which consists of multiple, logical operations. Example: if (x.started() && y.running()).Complex conditionals make the code even harder to read, and contribute to the Complex Method code smell. Encapsulate them.

How to fix it?

Apply the DECOMPOSE CONDITIONAL refactoring so that the complex conditional is encapsulated in a separate function with a good name that captures the business rule. Optionally, for simple expressions, introduce a new variable which holds the result of the complex conditional.

Helpful refactoring examples

To get a general understanding of what this code health issue looks like - and how it might be addressed - we have prepared some diffs for illustrative purposes.

SAMPLE

# complex_conditional.js
 function messageReceived(message, timeReceived) {
-   // Ignore all messages which aren't from known customers:
-   if (!message.sender &&
-       customers.getId(message.name) == null) {
+   // Refactoring #1: encapsulate the business rule in a
+   // function. A clear name replaces the need for the comment:
+   if (!knownCustomer(message)) {
      log('spam received -- ignoring');
      return;
    }
 
-  // Provide an auto-reply when outside business hours:
-  if ((timeReceived.getHours() > 17) ||
-      (timeReceived.getHours() < 8)) {
+  // Refactoring #2: encapsulate the business rule.
+  // Again, note how a clear function name replaces the
+  // need for a code comment:
+  if (outsideBusinessHours(timeReceived)) {
     return autoReplyTo(message);
   }
 
   pingAgentFor(message);
+}
+
+function outsideBusinessHours(timeReceived) {
+  // Refactoring #3: replace magic numbers with
+  // symbols that communicate with the code reader:
+  const closingHour = 17;
+  const openingHour = 8;
+
+  const hours = timeReceived.getHours();
+
+  // Refactoring #4: simple conditional rules can
+  // be further clarified by introducing a variable:
+  const afterClosing = hours > closingHour;
+  const beforeOpening = hours < openingHour;
+
+  // Yeah -- look how clear the business rule is now!
+  return afterClosing || beforeOpening;
 }

@coderabbitai

This comment was marked as resolved.

@lodyai
lodyai Bot force-pushed the issue-108-bring-oversized-files-under-400-lines branch from 978b446 to 989256e Compare June 12, 2026 13:08
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Jun 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.

tests/unit/test_bump_context_and_output.py

Comment on lines +216 to +268

def test_result_message_grammar_and_path_rendering(
    manifest_count: int,
    document_count: int,
    readme_count: int,
    lockfile_count: int,
) -> None:
    """Any category combination renders correct grammar and unique paths."""
    root = Path("/ws")
    manifests = tuple(root / f"m{i}" / "Cargo.toml" for i in range(manifest_count))
    documents = tuple(root / f"doc{i}.md" for i in range(document_count))
    readmes = tuple(root / f"r{i}" / "README.md" for i in range(readme_count))
    lockfiles = tuple(root / f"l{i}" / "Cargo.lock" for i in range(lockfile_count))
    changes = bump_output.BumpChanges(
        manifests=manifests,
        documents=documents,
        lockfiles=lockfiles,
        transposed_readmes=readmes,
    )

    message = bump_output._format_result_message(
        changes, "1.2.3", dry_run=False, workspace_root=root
    )

    has_no_changes = not any((manifests, documents, readmes, lockfiles))
    if has_no_changes:
        assert message == "No manifest changes required; all versions already 1.2.3."
        return

    lines = message.splitlines()
    expected_body = [
        *(f"- {path.relative_to(root)}" for path in manifests),
        *(f"- {path.relative_to(root)} (documentation)" for path in documents),
        *(f"- {path.relative_to(root)} (readme)" for path in readmes),
        *(f"- {path.relative_to(root)} (lockfile)" for path in lockfiles),
    ]
    assert lines[1:] == expected_body

    categories = []
    if manifests:
        categories.append(f"{len(manifests)} manifest(s)")
    if documents:
        categories.append(f"{len(documents)} documentation file(s)")
    if readmes:
        categories.append(f"{len(readmes)} readme file(s)")
    if lockfiles:
        categories.append(f"{len(lockfiles)} lockfile(s)")
    expected_header = (
        f"Updated version to 1.2.3 in {_expected_description(categories)}:"
    )
    assert lines[0] == expected_header
    assert " and and " not in lines[0]
    if len(categories) >= 3:
        assert ", and " in lines[0]

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

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Jun 15, 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.

tests/unit/test_bump_context_and_output.py

Comment on lines +199 to +212

def _build_test_change_body(
    root: Path,
    manifests: tuple[Path, ...],
    documents: tuple[Path, ...],
    readmes: tuple[Path, ...],
    lockfiles: tuple[Path, ...],
) -> list[str]:
    """Build the expected body lines for a result-message assertion."""
    return [
        *(f"- {path.relative_to(root)}" for path in manifests),
        *(f"- {path.relative_to(root)} (documentation)" for path in documents),
        *(f"- {path.relative_to(root)} (readme)" for path in readmes),
        *(f"- {path.relative_to(root)} (lockfile)" for path in lockfiles),
    ]

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

@coderabbitai

This comment was marked as resolved.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

codescene-access[bot]

This comment was marked as outdated.

…actors

main independently landed this branch's bundled #95/#96/#97, refactored bump.py
(new `_CrateManifestOutcome` flow) and publish.py (#73/#96), and re-split the
bump tests into differently-named modules. This reconciles the #108 extraction
onto main's current tree as a single squashed change (the original 22 commits
are preserved on backup/issue-108-pre-rebase2-7df1480).

Landed cleanly (all now under the 400-line guideline):

- `lading/toml_coerce/` package (scalar/sequence/mapping coercers with
  structural pattern matching and numpy docstrings) + `config.py` (441->334)
  and `workspace/models.py` (576->194) consuming it via `functools.partial`.
- `lading/workspace/_coercion.py` + `graph_build.py` (builders out of
  `models`); `cli_options.py` (cyclopts args out of `cli.py`, 449->389);
  `bump_docs.py` condensed (402->393).
- `bump_manifests.py` re-derived against main's bump.py: the low-level manifest
  helpers, `_WORKSPACE_SELECTORS`, and `_BumpContext` move here; main's
  `_apply_crate_manifest_update`/`_CrateManifestOutcome` flow stays in `bump`
  and imports them. `bump.py` 552->427 (further trim is follow-up).

Test reconciliation: main's bump-test split is authoritative; this drops the
branch's parallel split modules and keeps main's plus the unique
`test_toml_coerce.py`. Per the no-compat-alias convention (#164),
`test_dependency_sections` now reads `_DEPENDENCY_SECTION_BY_KIND` from
`bump_manifests`.

Deferred: `publish.py` (741) extraction. main's #73/#96 refactored the
extracted pipeline functions and its tests patch `publish._*` seams; extracting
now requires rewriting many `monkeypatch.setattr` targets to `publish_pipeline`
(no-compat), which needs a focused full-context pass. publish.py is taken from
main unchanged; the orphaned `publish_pipeline.py` is removed.

Validated: check-fmt, lint (10.00/10), typecheck, test (717 passed, 70
snapshots), markdownlint, nixie — all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lodyai
lodyai Bot force-pushed the issue-108-bring-oversized-files-under-400-lines branch from 7df1480 to e53d6e1 Compare July 8, 2026 21:17
codescene-access[bot]

This comment was marked as outdated.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
lading/toml_coerce/_sequences.py (1)

102-141: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

string_matrix's hand-built message also skips _reject.

_validate_matrix_entry correctly raises via _reject at every branch, but string_matrix builds its own message string and calls error(message) directly, dropping the "; received {type}." suffix that the canonical shape requires. This is a pre-existing pattern (the value it produces is pinned by the test_coercion_error_messages_are_stable snapshot for preflight.aux_build), so fix it deliberately alongside a snapshot regeneration rather than leaving it as the odd one out in an otherwise-uniform module.

♻️ Proposed fix (requires regenerating the syrupy snapshot)
 def string_matrix(
     value: object, field_name: str, *, error: _ErrorType
 ) -> tuple[tuple[str, ...], ...]:
     """Return a tuple-of-tuples parsed from ``value`` as string sequences."""
-    message = f"{field_name} must be a sequence of string sequences."
     match value:
         case None:
             return ()
         case str() | bytes():
-            raise error(message)
+            raise _reject(value, field_name, "a sequence of string sequences", error)
         case cabc.Sequence():
             return tuple(
                 _validate_matrix_entry(entry, field_name, index, error)
                 for index, entry in enumerate(value)
             )
         case _:
-            raise error(message)
+            raise _reject(value, field_name, "a sequence of string sequences", error)
🤖 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 `@lading/toml_coerce/_sequences.py` around lines 102 - 141, The string_matrix
function is bypassing the standard _reject-based error shape by raising
error(message) directly, so its failures miss the canonical "; received {type}."
suffix. Update string_matrix to use the same _reject pattern as
_validate_matrix_entry, keeping the error wording consistent with the module’s
other coercion helpers, and then regenerate the affected snapshot/test
expectation for test_coercion_error_messages_are_stable (preflight.aux_build).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/users-guide.md`:
- Around line 295-301: The fenced text example in the users guide exceeds the
120-column code-block wrap limit. Reformat the aggregated-error example in the
markdown snippet so the long “Cargo lockfile regeneration failed...” sentence is
split across multiple lines while preserving the existing meaning and the `cargo
update --workspace --manifest-path ...` lines; use the example block in
`docs/users-guide.md` to locate it, then run `make fmt` to verify the
markdownlint-friendly wrapping.

In `@lading/commands/bump_manifests.py`:
- Around line 46-48: The `_BumpContext` docstring contains a stale Sphinx
cross-reference that points to `_update_crate_manifest`, which no longer exists.
Update the reference in `_BumpContext` to use
`bump._apply_crate_manifest_update` so the documentation resolves correctly and
matches the current consumer of the context.

In `@lading/toml_coerce/_scalars.py`:
- Around line 99-120: non_negative_int is duplicating the canonical
error-message format instead of using the centralized helper. Update the
type-check and parse-failure paths in non_negative_int to call _reject with the
field name and expected type information, and use the same helper for the
negative-value case if that is also intended to share the canonical wording;
keep the existing control flow and identifiers like non_negative_int and _reject
so _core.py remains the single source of truth.

In `@lading/toml_coerce/_sequences.py`:
- Around line 35-54: The disallowed-None branch in expect_sequence uses a
one-off error message instead of the module’s shared rejection shape. Update the
None case in expect_sequence to route through _reject, matching the same
"{field_name} must be ...; received {type(x).__name__}." pattern used by the
string/bytes and fallback branches. Keep allow_none returning None unchanged,
and preserve the existing expected wording for a sequence.

In `@lading/workspace/graph_build.py`:
- Around line 1-8: Update the module docstring in graph_build.py so it no longer
says the coercion bindings are defined in models; change that reference to
lading.workspace._coercion to match the current import source. Keep the rest of
the description intact and make sure the wording still points maintainers to the
correct place for coercion-related validation behavior in this module.

---

Outside diff comments:
In `@lading/toml_coerce/_sequences.py`:
- Around line 102-141: The string_matrix function is bypassing the standard
_reject-based error shape by raising error(message) directly, so its failures
miss the canonical "; received {type}." suffix. Update string_matrix to use the
same _reject pattern as _validate_matrix_entry, keeping the error wording
consistent with the module’s other coercion helpers, and then regenerate the
affected snapshot/test expectation for test_coercion_error_messages_are_stable
(preflight.aux_build).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bd0304ff-1a83-4fcb-9b78-8579aaaf77e3

📥 Commits

Reviewing files that changed from the base of the PR and between 64668d3 and e53d6e1.

📒 Files selected for processing (23)
  • docs/developers-guide.md
  • docs/users-guide.md
  • lading/cli.py
  • lading/cli_options.py
  • lading/commands/bump.py
  • lading/commands/bump_docs.py
  • lading/commands/bump_manifests.py
  • lading/config.py
  • lading/toml_coerce/__init__.py
  • lading/toml_coerce/_core.py
  • lading/toml_coerce/_mappings.py
  • lading/toml_coerce/_scalars.py
  • lading/toml_coerce/_sequences.py
  • lading/workspace/__init__.py
  • lading/workspace/_coercion.py
  • lading/workspace/graph_build.py
  • lading/workspace/models.py
  • tests/bdd/steps/test_bump_steps.py
  • tests/unit/__snapshots__/test_toml_coerce.ambr
  • tests/unit/test_cli.py
  • tests/unit/test_dependency_sections.py
  • tests/unit/test_toml_coerce.py
  • tests/unit/test_workspace_models_validation.py
🔗 Linked repositories identified

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

  • leynos/cmd-mox (auto-detected)
  • leynos/shared-actions (auto-detected)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
lading/toml_coerce/_sequences.py (1)

102-141: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

string_matrix's hand-built message also skips _reject.

_validate_matrix_entry correctly raises via _reject at every branch, but string_matrix builds its own message string and calls error(message) directly, dropping the "; received {type}." suffix that the canonical shape requires. This is a pre-existing pattern (the value it produces is pinned by the test_coercion_error_messages_are_stable snapshot for preflight.aux_build), so fix it deliberately alongside a snapshot regeneration rather than leaving it as the odd one out in an otherwise-uniform module.

♻️ Proposed fix (requires regenerating the syrupy snapshot)
 def string_matrix(
     value: object, field_name: str, *, error: _ErrorType
 ) -> tuple[tuple[str, ...], ...]:
     """Return a tuple-of-tuples parsed from ``value`` as string sequences."""
-    message = f"{field_name} must be a sequence of string sequences."
     match value:
         case None:
             return ()
         case str() | bytes():
-            raise error(message)
+            raise _reject(value, field_name, "a sequence of string sequences", error)
         case cabc.Sequence():
             return tuple(
                 _validate_matrix_entry(entry, field_name, index, error)
                 for index, entry in enumerate(value)
             )
         case _:
-            raise error(message)
+            raise _reject(value, field_name, "a sequence of string sequences", error)
🤖 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 `@lading/toml_coerce/_sequences.py` around lines 102 - 141, The string_matrix
function is bypassing the standard _reject-based error shape by raising
error(message) directly, so its failures miss the canonical "; received {type}."
suffix. Update string_matrix to use the same _reject pattern as
_validate_matrix_entry, keeping the error wording consistent with the module’s
other coercion helpers, and then regenerate the affected snapshot/test
expectation for test_coercion_error_messages_are_stable (preflight.aux_build).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/users-guide.md`:
- Around line 295-301: The fenced text example in the users guide exceeds the
120-column code-block wrap limit. Reformat the aggregated-error example in the
markdown snippet so the long “Cargo lockfile regeneration failed...” sentence is
split across multiple lines while preserving the existing meaning and the `cargo
update --workspace --manifest-path ...` lines; use the example block in
`docs/users-guide.md` to locate it, then run `make fmt` to verify the
markdownlint-friendly wrapping.

In `@lading/commands/bump_manifests.py`:
- Around line 46-48: The `_BumpContext` docstring contains a stale Sphinx
cross-reference that points to `_update_crate_manifest`, which no longer exists.
Update the reference in `_BumpContext` to use
`bump._apply_crate_manifest_update` so the documentation resolves correctly and
matches the current consumer of the context.

In `@lading/toml_coerce/_scalars.py`:
- Around line 99-120: non_negative_int is duplicating the canonical
error-message format instead of using the centralized helper. Update the
type-check and parse-failure paths in non_negative_int to call _reject with the
field name and expected type information, and use the same helper for the
negative-value case if that is also intended to share the canonical wording;
keep the existing control flow and identifiers like non_negative_int and _reject
so _core.py remains the single source of truth.

In `@lading/toml_coerce/_sequences.py`:
- Around line 35-54: The disallowed-None branch in expect_sequence uses a
one-off error message instead of the module’s shared rejection shape. Update the
None case in expect_sequence to route through _reject, matching the same
"{field_name} must be ...; received {type(x).__name__}." pattern used by the
string/bytes and fallback branches. Keep allow_none returning None unchanged,
and preserve the existing expected wording for a sequence.

In `@lading/workspace/graph_build.py`:
- Around line 1-8: Update the module docstring in graph_build.py so it no longer
says the coercion bindings are defined in models; change that reference to
lading.workspace._coercion to match the current import source. Keep the rest of
the description intact and make sure the wording still points maintainers to the
correct place for coercion-related validation behavior in this module.

---

Outside diff comments:
In `@lading/toml_coerce/_sequences.py`:
- Around line 102-141: The string_matrix function is bypassing the standard
_reject-based error shape by raising error(message) directly, so its failures
miss the canonical "; received {type}." suffix. Update string_matrix to use the
same _reject pattern as _validate_matrix_entry, keeping the error wording
consistent with the module’s other coercion helpers, and then regenerate the
affected snapshot/test expectation for test_coercion_error_messages_are_stable
(preflight.aux_build).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bd0304ff-1a83-4fcb-9b78-8579aaaf77e3

📥 Commits

Reviewing files that changed from the base of the PR and between 64668d3 and e53d6e1.

📒 Files selected for processing (23)
  • docs/developers-guide.md
  • docs/users-guide.md
  • lading/cli.py
  • lading/cli_options.py
  • lading/commands/bump.py
  • lading/commands/bump_docs.py
  • lading/commands/bump_manifests.py
  • lading/config.py
  • lading/toml_coerce/__init__.py
  • lading/toml_coerce/_core.py
  • lading/toml_coerce/_mappings.py
  • lading/toml_coerce/_scalars.py
  • lading/toml_coerce/_sequences.py
  • lading/workspace/__init__.py
  • lading/workspace/_coercion.py
  • lading/workspace/graph_build.py
  • lading/workspace/models.py
  • tests/bdd/steps/test_bump_steps.py
  • tests/unit/__snapshots__/test_toml_coerce.ambr
  • tests/unit/test_cli.py
  • tests/unit/test_dependency_sections.py
  • tests/unit/test_toml_coerce.py
  • tests/unit/test_workspace_models_validation.py
🔗 Linked repositories identified

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

  • leynos/cmd-mox (auto-detected)
  • leynos/shared-actions (auto-detected)
🛑 Comments failed to post (5)
docs/users-guide.md (1)

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

Wrap the aggregated-error example past the 120-column code limit.

Line 296 runs to roughly 166 characters — break it across lines so the fenced text block stays inside the documented 120-column wrap for code blocks.

✏️ Proposed fix
 ```text
-Cargo lockfile regeneration failed for 2 manifest(s). Manifests already carry the new version, so the workspace is inconsistent until each lockfile below is repaired:
+Cargo lockfile regeneration failed for 2 manifest(s). Manifests already carry
+the new version, so the workspace is inconsistent until each lockfile below
+is repaired:
 - Cargo lockfile regeneration failed for crates/a/Cargo.toml with exit code 101: <cargo error>
   cargo update --workspace --manifest-path crates/a/Cargo.toml
 - Cargo lockfile regeneration failed for crates/b/Cargo.toml with exit code 101: <cargo error>
   cargo update --workspace --manifest-path crates/b/Cargo.toml
</details>

Run `make fmt` afterwards to confirm the fix satisfies `make markdownlint`.






Triage: `[type:syntax/md]`

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🤖 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 `@docs/users-guide.md` around lines 295 - 301, The fenced text example in the
users guide exceeds the 120-column code-block wrap limit. Reformat the
aggregated-error example in the markdown snippet so the long “Cargo lockfile
regeneration failed...” sentence is split across multiple lines while preserving
the existing meaning and the `cargo update --workspace --manifest-path ...`
lines; use the example block in `docs/users-guide.md` to locate it, then run
`make fmt` to verify the markdownlint-friendly wrapping.

Sources: Coding guidelines, Path instructions

lading/commands/bump_manifests.py (1)

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

Fix the stale :func: cross-reference in _BumpContext.

The docstring points at _update_crate_manifest, which no longer exists after the extraction. The context is now consumed by bump._apply_crate_manifest_update, so the Sphinx cross-reference resolves to nothing.

✏️ Proposed fix
-    Derived once by ``bump._initialize_bump_context`` and consumed by
-    :func:`_update_crate_manifest`; the manifest-mutation contract lives with
-    this extracted module rather than reaching back into ``bump`` internals.
+    Derived once by ``bump._initialize_bump_context`` and consumed by
+    ``bump._apply_crate_manifest_update``; the manifest-mutation contract lives
+    with this extracted module rather than reaching back into ``bump`` internals.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    Derived once by ``bump._initialize_bump_context`` and consumed by
    ``bump._apply_crate_manifest_update``; the manifest-mutation contract lives
    with this extracted module rather than reaching back into ``bump`` internals.
🤖 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 `@lading/commands/bump_manifests.py` around lines 46 - 48, The `_BumpContext`
docstring contains a stale Sphinx cross-reference that points to
`_update_crate_manifest`, which no longer exists. Update the reference in
`_BumpContext` to use `bump._apply_crate_manifest_update` so the documentation
resolves correctly and matches the current consumer of the context.
lading/toml_coerce/_scalars.py (1)

99-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse _reject instead of hand-rolling the canonical message.

non_negative_int builds type_error manually rather than calling _reject, duplicating the exact message shape that _core.py exists to centralize. Route all three raise sites through _reject so this module stays a single source of truth for the canonical format.

♻️ Proposed fix
-    type_error = f"{field_name} must be an integer; received {type(value).__name__}."
-    # ``bool`` is a subclass of ``int`` and ``float``/other types are truthy for
-    # a blanket ``int(...)`` cast, so dispatch explicitly to accept only real
-    # integers and integer-valued strings (the config string path).
+    # ``bool`` is a subclass of ``int`` and ``float``/other types are truthy for
+    # a blanket ``int(...)`` cast, so dispatch explicitly to accept only real
+    # integers and integer-valued strings (the config string path).
     match value:
         case None:
             return default
         case bool():
-            raise error(type_error)
+            raise _reject(value, field_name, "an integer", error)
         case int():
             integer = value
         case str():
             try:
                 integer = int(value)
             except ValueError as exc:
-                raise error(type_error) from exc
+                raise _reject(value, field_name, "an integer", error) from exc
         case _:
-            raise error(type_error)
+            raise _reject(value, field_name, "an integer", error)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    # ``bool`` is a subclass of ``int`` and ``float``/other types are truthy for
    # a blanket ``int(...)`` cast, so dispatch explicitly to accept only real
    # integers and integer-valued strings (the config string path).
    match value:
        case None:
            return default
        case bool():
            raise _reject(value, field_name, "an integer", error)
        case int():
            integer = value
        case str():
            try:
                integer = int(value)
            except ValueError as exc:
                raise _reject(value, field_name, "an integer", error) from exc
        case _:
            raise _reject(value, field_name, "an integer", error)
    if integer < 0:
        message = f"{field_name} must be non-negative."
        raise error(message)
    return integer
🤖 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 `@lading/toml_coerce/_scalars.py` around lines 99 - 120, non_negative_int is
duplicating the canonical error-message format instead of using the centralized
helper. Update the type-check and parse-failure paths in non_negative_int to
call _reject with the field name and expected type information, and use the same
helper for the negative-value case if that is also intended to share the
canonical wording; keep the existing control flow and identifiers like
non_negative_int and _reject so _core.py remains the single source of truth.
lading/toml_coerce/_sequences.py (1)

35-54: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Route the disallowed-None branch through _reject for a consistent error shape.

The None-not-allowed branch raises error(message) directly with message = f"{field_name} must be a sequence" — no "; received {type}." suffix. Every other rejection path in this module (and _reject itself) follows "{field} must be {expected}; received {type(x).__name__}." — this one line breaks that contract without a documented reason.

🐛 Proposed fix for the canonical message shape
     match value:
         case None:
             if allow_none:
                 return None
-            message = f"{field_name} must be a sequence"
-            raise error(message)
+            raise _reject(value, field_name, "a sequence", error)

Based on the PR objective to "Keep the shared error-message shape: {field} must be … received {type(x).__name__}", this branch should follow the same pattern as the rest of the module.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    match value:
        case None:
            if allow_none:
                return None
            raise _reject(value, field_name, "a sequence", error)
        case str() | bytes() | bytearray():
            raise _reject(value, field_name, "a sequence", error)
        case cabc.Sequence():
            return value
        case _:
            raise _reject(value, field_name, "a sequence", error)
🤖 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 `@lading/toml_coerce/_sequences.py` around lines 35 - 54, The disallowed-None
branch in expect_sequence uses a one-off error message instead of the module’s
shared rejection shape. Update the None case in expect_sequence to route through
_reject, matching the same "{field_name} must be ...; received
{type(x).__name__}." pattern used by the string/bytes and fallback branches.
Keep allow_none returning None unchanged, and preserve the existing expected
wording for a sequence.
lading/workspace/graph_build.py (1)

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

Fix the stale "coercion bindings defined in models" claim.

The bindings moved to lading.workspace._coercion (confirmed by the import block at line 19), yet the module docstring still says they're "defined in models". Update the reference so a maintainer debugging a coercion failure looks in the right module.

✏️ Proposed fix
 Extracted from :mod:`lading.workspace.models` (issue `#108`) so the data
 structures and topology stay separate from the metadata-parsing layer. The
-module shares the coercion bindings defined in ``models`` so all workspace
+module shares the coercion bindings defined in :mod:`lading.workspace._coercion`
+so all workspace
 validation failures raise :class:`WorkspaceModelError` with the canonical
 message shape from :mod:`lading.toml_coerce`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

"""Builders converting ``cargo metadata`` output into workspace models.

Extracted from :mod:`lading.workspace.models` (issue `#108`) so the data
structures and topology stay separate from the metadata-parsing layer. The
module shares the coercion bindings defined in :mod:`lading.workspace._coercion`
so all workspace validation failures raise :class:`WorkspaceModelError` with
the canonical message shape from :mod:`lading.toml_coerce`.
"""
🤖 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 `@lading/workspace/graph_build.py` around lines 1 - 8, Update the module
docstring in graph_build.py so it no longer says the coercion bindings are
defined in models; change that reference to lading.workspace._coercion to match
the current import source. Keep the rest of the description intact and make sure
the wording still points maintainers to the correct place for coercion-related
validation behavior in this module.

Centralise the canonical error-message shape in the toml_coerce helpers:
- `non_negative_int` type/parse-failure arms now raise
  `_reject(value, field_name, "an integer", error)` (identical message, so the
  snapshot is unchanged); the `str` arm preserves `from exc` chaining. The
  negative-value message stays a plain value error (the `"; received {type}."`
  type-shape does not fit it).
- `expect_sequence`'s None-disallowed branch and both `string_matrix` failure
  arms now route through `_reject`, so they carry the canonical
  `"; received {type}."` suffix. Regenerated the `preflight.aux_build` line of
  the `test_coercion_error_messages_are_stable` snapshot accordingly.

Docstrings: `_BumpContext` now points at `bump._apply_crate_manifest_update`
(the current consumer; `_update_crate_manifest` was removed), and
`graph_build`'s module docstring points coercion bindings at
`lading.workspace._coercion` rather than `models`.

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

This comment was marked as outdated.

Address the "canonical-message branches unguarded" review finding by asserting
exact error wording (not just the raised type) on the branches that recently
moved to `_reject`, and by covering the `expect_mapping` success path:

- `test_expect_sequence_handles_none` now asserts the disallowed-None message
  `"f must be a sequence; received NoneType."`.
- `test_string_matrix_rejects_non_sequence_values` asserts the top-level
  `"... must be a sequence of string sequences; received <type>."` shape.
- `test_string_matrix_rejects_non_string_rows` asserts the row-level
  `"demo.matrix[1] must be a sequence of strings; received <type>."` shape.
- `test_mapping_helpers_reject_non_mappings` becomes
  `test_mapping_helpers_accept_and_reject_mappings`, adding a passthrough
  assertion that a valid mapping is returned unchanged.

Tests are strengthened in place (no new methods) to stay within the
`TestTomlCoerce` PLR0904 public-method cap. The graph_build module-docstring
finding was already fixed in the prior commit.

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

This comment was marked as outdated.

@leynos

leynos commented Jul 9, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai please create a new GitHub issue for the following:

Skipped — Architectural Complexity (warning): getting bump.py under 400 needs further extraction (docstring trimming alone can't; ~27 lines over), and publish.py (741) is the deliberately-deferred extraction — it requires reconciling main's #73/#96 refactor and rewriting many monkeypatch.setattr(publish, …) seams to publish_pipeline per the no-compat convention. That's a substantial, non-minimal task unsuited to this review-fix pass; it's tracked as the follow-up we discussed. Everything actionable-and-minimal here is done.

@leynos

leynos commented Jul 9, 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.

tests/unit/test_toml_coerce.py

Comment on lines +99 to +111

    def test_string_tuple_rejects_non_string_entries(
        self,
        values: list[str],
        bad_index: int,
        bad: object,
        error: type[Exception],
    ) -> None:
        """A non-string entry is rejected with its index in the field name."""
        _assert_rejects_indexed_non_string(
            toml_coerce.string_tuple,
            "demo.list",
            _IndexedRejectionCase(values, bad_index, bad, error),
        )

❌ New issue: Code Duplication
The module contains 4 functions with similar structure: TestTomlCoerce.test_string_matrix_rejects_non_sequence_values,TestTomlCoerce.test_string_matrix_rejects_non_string_rows,TestTomlCoerce.test_string_tuple_rejects_non_string_entries,TestTomlCoerce.test_validate_string_sequence_rejects_non_strings

@leynos

leynos commented Jul 9, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Have the following now been resolved?

Annul any requirements that violate the en-GB-oxendict spelling (-ize / -yse / -our) conventions (for example a request to replace "normalize" with "normalise" or "artefact" with "artifact"), or where the requirement unnecessarily increases cyclomatic complexity.

Inline comments:

In @lading/commands/bump_manifests.py:

  • Around line 46-48: The _BumpContext docstring contains a stale Sphinx
    cross-reference that points to _update_crate_manifest, which no longer exists.
    Update the reference in _BumpContext to use
    bump._apply_crate_manifest_update so the documentation resolves correctly and
    matches the current consumer of the context.

In @lading/toml_coerce/_scalars.py:

  • Around line 99-120: non_negative_int is duplicating the canonical
    error-message format instead of using the centralized helper. Update the
    type-check and parse-failure paths in non_negative_int to call _reject with the
    field name and expected type information, and use the same helper for the
    negative-value case if that is also intended to share the canonical wording;
    keep the existing control flow and identifiers like non_negative_int and _reject
    so _core.py remains the single source of truth.

In @lading/toml_coerce/_sequences.py:

  • Around line 35-54: The disallowed-None branch in expect_sequence uses a
    one-off error message instead of the module’s shared rejection shape. Update the
    None case in expect_sequence to route through _reject, matching the same
    "{field_name} must be ...; received {type(x).name}." pattern used by the
    string/bytes and fallback branches. Keep allow_none returning None unchanged,
    and preserve the existing expected wording for a sequence.

In @lading/workspace/graph_build.py:

  • Around line 1-8: Update the module docstring in graph_build.py so it no longer
    says the coercion bindings are defined in models; change that reference to
    lading.workspace._coercion to match the current import source. Keep the rest of
    the description intact and make sure the wording still points maintainers to the
    correct place for coercion-related validation behavior in this module.

Outside diff comments:
In @lading/toml_coerce/_sequences.py:

  • Around line 102-141: The string_matrix function is bypassing the standard
    _reject-based error shape by raising error(message) directly, so its failures
    miss the canonical "; received {type}." suffix. Update string_matrix to use the
    same _reject pattern as _validate_matrix_entry, keeping the error wording
    consistent with the module’s other coercion helpers, and then regenerate the
    affected snapshot/test expectation for test_coercion_error_messages_are_stable
    (preflight.aux_build).

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="leynos/lading"

body=$(cat <<'EOF'
## Summary

Follow-up from PR `#136` (issue `#108`): two source files still exceed the project's 400-line guideline and require further extraction beyond what was completed in that PR.

## Affected files

- `lading/commands/bump.py` — still ~27 lines over the 400-line limit. Docstring trimming alone cannot close the gap; further logic extraction is required (in the spirit of the `bump_manifests.py` split already landed).
- `lading/commands/publish.py` — 741 lines, deliberately deferred from PR `#136`.

## Rationale for deferral

Extracting `publish.py` is a substantial, non-minimal task because it requires:

1. Reconciling main's `#73/`#96 refactor with the pipeline extraction done in PR `#136` (`lading/commands/publish_pipeline.py`).
2. Rewriting the many `monkeypatch.setattr(publish, …)` test seams to target `publish_pipeline` instead, per the project's no-compat convention (no shim/back-compat layers).

This was judged unsuited to the review-fix pass on PR `#136` and tracked here as the agreed follow-up.

## Acceptance criteria

- [ ] `lading/commands/bump.py` is at or below 400 lines through further logic extraction (not just docstring trimming).
- [ ] `lading/commands/publish.py` is at or below 400 lines, with responsibilities distributed to `publish_pipeline.py` and/or other new modules as appropriate.
- [ ] All `monkeypatch.setattr(publish, …)` test seams are updated to target the correct module post-extraction, with no compatibility shims left behind.
- [ ] `docs/developers-guide.md` extraction map is updated to reflect the new module boundaries.
- [ ] `make lint`, `make typecheck`, and `make test` pass.

## References

- PR: https://github.com/leynos/lading/pull/136
- Requested by: `@leynos`
EOF
)

gh issue create \
  --repo "$repo" \
  --title "Further extraction needed: bump.py and publish.py still exceed 400-line guideline" \
  --assignee "leynos" \
  --body "$body"

Length of output: 3908


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="leynos/lading"

body=$(cat <<'EOF'
## Summary

Follow-up from PR `#136` (issue `#108`): two source files still exceed the project's 400-line guideline and require further extraction beyond what was completed in that PR.

## Affected files

- `lading/commands/bump.py` — still ~27 lines over the 400-line limit. Docstring trimming alone cannot close the gap; further logic extraction is required (in the spirit of the `bump_manifests.py` split already landed).
- `lading/commands/publish.py` — 741 lines, deliberately deferred from PR `#136`.

## Rationale for deferral

Extracting `publish.py` is a substantial, non-minimal task because it requires:

1. Reconciling main's `#73/`#96 refactor with the pipeline extraction done in PR `#136` (`lading/commands/publish_pipeline.py`).
2. Rewriting the many `monkeypatch.setattr(publish, …)` test seams to target `publish_pipeline` instead, per the project's no-compat convention (no shim/back-compat layers).

This was judged unsuited to the review-fix pass on PR `#136` and tracked here as the agreed follow-up.

## Acceptance criteria

- [ ] `lading/commands/bump.py` is at or below 400 lines through further logic extraction (not just docstring trimming).
- [ ] `lading/commands/publish.py` is at or below 400 lines, with responsibilities distributed to `publish_pipeline.py` and/or other new modules as appropriate.
- [ ] All `monkeypatch.setattr(publish, …)` test seams are updated to target the correct module post-extraction, with no compatibility shims left behind.
- [ ] `docs/developers-guide.md` extraction map is updated to reflect the new module boundaries.
- [ ] `make lint`, `make typecheck`, and `make test` pass.

## References

- PR: https://github.com/leynos/lading/pull/136
- Requested by: `@leynos`
EOF
)

gh issue create \
  --repo "$repo" \
  --title "Further extraction needed: bump.py and publish.py still exceed 400-line guideline" \
  --assignee "leynos" \
  --body "$body"

Length of output: 2151


@leynos, created #170 to track the deferred extraction work: #170

It covers both remaining oversized files, lading/commands/bump.py (~27 lines over) and lading/commands/publish.py (741 lines), and records the rationale for deferring the publish.py split — reconciling main's #73/#96 refactor and rewriting the monkeypatch.setattr(publish, …) seams to publish_pipeline under the no-compat convention. Assigned to you, with backlinks to PR #136.

(•‿•)

@coderabbitai

This comment was marked as resolved.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "--- bump_manifests.py _BumpContext docstring ---"
rg -n "_BumpContext" -A 20 lading/commands/bump_manifests.py | head -40

echo "--- _apply_crate_manifest_update / _update_crate_manifest usage ---"
rg -n "_apply_crate_manifest_update|_update_crate_manifest" lading/commands/*.py

echo "--- non_negative_int ---"
rg -n "" lading/toml_coerce/_scalars.py | sed -n '1,140p'

echo "--- expect_sequence ---"
rg -n "" lading/toml_coerce/_sequences.py | sed -n '1,145p'

echo "--- graph_build.py docstring ---"
sed -n '1,15p' lading/workspace/graph_build.py

echo "--- string_matrix / _validate_matrix_entry ---"
rg -n "_reject|error\(" lading/toml_coerce/_sequences.py

Length of output: 12122


🏁 Script executed:

#!/bin/bash
echo "--- snapshot file ---"
cat tests/unit/__snapshots__/test_toml_coerce.ambr

echo "--- test using preflight.aux_build ---"
rg -n "aux_build|preflight" tests/unit/test_toml_coerce.py -A3 -B3

Length of output: 1847


@leynos, checked against the current codegraph. Status:

  1. lading/commands/bump_manifests.py_BumpContext docstring: Resolved. It now reads :func:bump._apply_crate_manifest_update`` (Line 47), matching the current consumer.

  2. lading/toml_coerce/_scalars.pynon_negative_int: Mostly resolved. The type-check branch (bool), the fallback branch, and the parse-failure branch (ValueError on int(value)) all now route through _reject (Lines 106, 113, 115). The negative-value branch (Lines 116–118) still builds its own message and calls error(message) directly rather than _reject. That's fine to leave as-is: _reject's canonical shape appends "; received {type}.", which is redundant for a range failure where the type is already known to be int — forcing it there would add noise rather than clarity. Treat that half of the original request as optional and already satisfied in spirit; no further change needed unless you want strict uniformity.

  3. lading/toml_coerce/_sequences.pyexpect_sequence: Resolved. The disallowed-None branch now raises via _reject(value, field_name, "a sequence", error) (Line 47), matching the shared shape used by the string/bytes and fallback branches.

  4. lading/workspace/graph_build.py — module docstring: Resolved. It now states the module shares coercion bindings defined in :mod:lading.workspace._coercion`` rather than models.

  5. lading/toml_coerce/_sequences.pystring_matrix (outside-diff): Resolved. Both string_matrix and _validate_matrix_entry raise exclusively via _reject, giving the canonical "; received {type}." suffix throughout.

  6. Snapshot regeneration for test_coercion_error_messages_are_stable: Confirmed. The stored snapshot line for preflight.aux_build now reads 'preflight.aux_build must be a sequence of string sequences; received int.', matching the canonical shape — the snapshot was regenerated correctly.

No spelling-convention (-ise/-yse/-our) or complexity-increasing requirements survive in the applied changes; everything above already lands on the en-GB-oxendict side and none of it adds branching. Nothing further needed from this batch.

(=^・ω・^=) all five plus the outside-diff item check out

Resolve the CodeScene "Code Duplication" finding across the four rejection
tests. Extract the common raise/capture/assert-substring/assert-type-name tail
into a generic `_assert_rejection(call, expected_prefix, bad_value, error)`
helper. `_assert_rejects_indexed_non_string` now builds the indexed input and
delegates to it, and the two `string_matrix` rejection tests call it directly
instead of repeating the tail inline. The `string_tuple` and
`validate_string_sequence` rejection tests are unchanged — they already route
through `_assert_rejects_indexed_non_string`.

All four tests stay distinct, independently discoverable methods; assertion
semantics and field-name literals are unchanged. (The helper's summary
docstring is wrapped to keep it within the line-length limit.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@leynos
leynos merged commit f6c3489 into main Jul 9, 2026
6 checks passed
@leynos
leynos deleted the issue-108-bring-oversized-files-under-400-lines branch July 9, 2026 10:27
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.

Bring oversized files under the 400-line guideline; extract shared toml_coerce module

2 participants