Skip to content

Regenerate discovered nested lockfiles during lading bump (regenerate-lockfiles) - #160

Merged
leynos merged 31 commits into
mainfrom
regenerate-lockfiles
Aug 1, 2026
Merged

Regenerate discovered nested lockfiles during lading bump (regenerate-lockfiles)#160
leynos merged 31 commits into
mainfrom
regenerate-lockfiles

Conversation

@leynos

@leynos leynos commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

This branch makes lading bump regenerate every git-tracked Cargo.lock in the repository, not just the workspace root plus manifests listed in bump.lockfile_manifests. Previously, lading publish validated all tracked lockfiles under --locked, so any nested fixture lockfile that bump had not been configured to refresh went stale and publish aborted with a manual repair message — leaving the user wondering why bump had not done the repair itself. The users' guide already promised discovery-based refresh; this branch makes the code keep that promise.

Execplan: docs/execplans/regenerate-lockfiles.md (implemented in full; status COMPLETE, including the Stage A prototype evidence, red/green test transcripts, an end-to-end acceptance run, and the retrospective).

The branch also carries three supporting changes:

  • The publish pre-flight stale-lockfile message no longer blames lading bump; it now names the remaining causes (manifest edits made without bump, or bump runs with --no-rebuild-lockfiles).
  • The make typecheck gate is restored and future-proofed: six pre-existing diagnostics under the drifting unpinned ty toolchain are fixed, and ty is now pinned (TY_VERSION ?= 0.0.56) in the Makefile with CI running the pinned version via make typecheck rather than installing ty separately.
  • A formatting-only mdformat reflow of the developers' guide is committed separately to keep future make fmt runs clean.

Review walkthrough

Validation

  • make test: 683 passed, 62 snapshots passed.
  • make lint: Ruff clean; interrogate 100%; Pylint 10.00/10.
  • make check-fmt: 132 files already formatted.
  • make typecheck (ty 0.0.56, pinned): all checks passed.
  • make markdownlint and make nixie: clean.
  • uv tool run mbake validate Makefile: valid syntax.
  • coderabbit review --agent: run after each milestone (feature, docs, ty pinning); zero findings each time.
  • End-to-end acceptance with real git and cargo against a prototype workspace mirroring the reported failure: bump discovered two tracked lockfiles, listed - fixtures/minimal/Cargo.lock (lockfile) in its output, refreshed the nested lockfile to the new version, and cargo metadata --locked subsequently exited 0.
  • Red/green evidence: the new unit tests and BDD scenario were observed failing for the expected reasons before implementation (transcripts recorded in the execplan).

Notes

  • Tests follow red-green-refactor; the red tests were committed together with the implementation so the suite stays green at every commit boundary.
  • One edge surfaced by the live acceptance run: a nested non-member package that pins a versioned path dependency on a bumped crate (alpha = { path = ..., version = "0.1.0" }) now fails at bump time with cargo's version-selection error, because bump does not rewrite non-member manifests. Such a repository already failed at publish time, so the error merely surfaces earlier and more actionably; rewriting those requirements is recorded in the execplan as an open follow-up.
  • bump.lockfile_manifests remains supported for lockfiles that git does not track; --no-rebuild-lockfiles remains the global escape hatch.
  • Regeneration is still non-atomic across manifests (pre-existing behaviour, documented; see issue Handle workspace inconsistency when lockfile refresh fails after manifest rewrites in bump #84).

References

@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 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 036a1e94-fe19-4eab-b566-844e9a392551

📥 Commits

Reviewing files that changed from the base of the PR and between fd3e38c and 926e04f.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • Makefile
  • docs/developers-guide.md
  • docs/execplans/regenerate-lockfiles.md
  • lading/commands/publish_plan.py
  • lading/workspace/models.py
  • tests/unit/publish/conftest.py
  • tests/unit/test_bump_lockfile_regeneration_metrics.py
  • tests/unit/test_lockfile.py
🔗 Linked repositories identified

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

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

Summary

  • Update lading bump to discover Git-tracked nested Cargo.lock files and merge their manifests with configured bump.lockfile_manifests.
  • Use the merged manifest set for dry-run reporting and live regeneration.
  • Fall back to configured manifests with a warning in non-Git workspaces.
  • Add validated, ordered, de-duplicated manifest resolution and resilient regeneration with aggregated repair errors.
  • Refactor lockfile handling into focused modules while preserving the public façade and repository boundary.
  • Add bounded regeneration metrics, injectable timing, and suppressed dry-run discovery telemetry.
  • Add unit, property-based, BDD, snapshot, and acceptance coverage.
  • Clarify user and developer documentation, stale-lockfile diagnostics, and non-atomic regeneration.
  • Add the completed Regenerate discovered nested lockfiles execplan.
  • Pin ty to 0.0.56 and Ruff to 0.15.21 consistently in the Makefile and CI.
  • Restore type-checking support through uv tool run --from ty==... ty.

Related references

  • Extend the cargo-and-git-backed lockfile repository design in docs/lading-design.md (issue #82).
  • Add coverage for aggregated regeneration failures (issue #84).
  • Retain path-validation coverage related to issue #93.

Walkthrough

lading bump now discovers tracked nested lockfiles, merges their manifests, validates targets, regenerates each lockfile, and aggregates failures. Tests and documentation cover the workflow. The PR also pins Ruff and ty, and refines publish validation diagnostics.

Changes

Nested lockfile regeneration

Layer / File(s) Summary
Manifest discovery and path contracts
lading/commands/bump_lockfile_manifests.py, lading/commands/bump_lockfile_paths.py, tests/unit/test_bump_lockfile_manifest_merge.py, tests/unit/test_bump_lockfile_path_resolution.py
Merge configured and tracked manifests, validate workspace boundaries, and derive ordered lockfile paths.
Cargo regeneration and façade wiring
lading/commands/bump_lockfile_regeneration.py, lading/commands/bump_lockfiles.py, tests/unit/test_bump_lockfiles.py, tests/unit/test_bump_lockfile_rebuild.py, tests/unit/conftest.py, tests/unit/test_bump_lockfile_regeneration_metrics.py
Run Cargo updates for each merged manifest, aggregate failures, record metrics, and preserve the façade.
Discovery observability and integration coverage
lading/commands/lockfile.py, tests/unit/test_lockfile.py, tests/bdd/features/cli.feature, tests/bdd/steps/test_bump_steps.py
Control discovery metrics and logs. Verify nested lockfile regeneration.
Lockfile workflow documentation
docs/execplans/regenerate-lockfiles.md, docs/users-guide.md, docs/developers-guide.md, docs/lading-design.md, docs/contents.md, docs/repository-layout.md
Document discovery, regeneration, stale-lock validation, metrics, and execution plans.

Pinned typechecking toolchain

Layer / File(s) Summary
Pinned typecheck execution
Makefile, .github/workflows/ci.yml, docs/developers-guide.md
Pin Ruff to 0.15.21, pin ty to 0.0.56, and align the ty invocation.

Publish validation control flow

Layer / File(s) Summary
Dependency placement control flow
lading/commands/publish_index_check.py
Use explicit early-return branches for missing dependency versions.
Stale lockfile diagnostics
lading/commands/publish_preflight.py, docs/users-guide.md, tests/unit/publish/__snapshots__/test_preflight_lockfile_validation.ambr
Document stale locks caused by direct manifest edits or --no-rebuild-lockfiles.

Documentation wording cleanup

Layer / File(s) Summary
Docstring wording
lading/commands/publish_plan.py, lading/workspace/models.py, tests/unit/publish/conftest.py
Clarify descriptive docstrings without changing behaviour.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant LadingBump
  participant Git
  participant Cargo
  User->>LadingBump: run lading bump
  LadingBump->>Git: discover tracked Cargo.lock files
  Git-->>LadingBump: return lockfile paths
  LadingBump->>Cargo: update workspace per merged manifest
  Cargo-->>LadingBump: return success or failure
  LadingBump-->>User: list refreshed lockfiles or repair commands
Loading

Possibly related PRs

Suggested reviewers: codescene-access

Poem

Nested locks emerge from Git,
Cargo updates each target bit;
ty is pinned, the paths align,
Failed repairs report in line.
Bump and publish now speak clear.


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (1 error, 1 warning, 3 inconclusive)

Check name Status Explanation Resolution
Unit Architecture ❌ Error resolve_lockfile_paths is presented as a path query, but its adapter calls merge_discovered_manifests, which runs git ls-files; the protocol documents only path errors, not discovery failures. Separate Git discovery from pure path projection, or expose and document the fallible discovery dependency and handle LockfileDiscoveryError and runner failures at the command boundary.
User-Facing Documentation ⚠️ Warning The user guide clearly documents lockfile discovery, dry-run behaviour, configuration, failures, and stale-lock repairs, but no n+1 migration document signposts this new behaviour for the 0.2.0 pre... Add a 0.3.0 migration document or migration section that explains tracked-lockfile discovery, dry-run output, configuration fallback, and the regeneration escape hatch.
Testing (Unit And Behavioural) ❓ Inconclusive Evidence gathering is still in progress. Inspect the full unit and behavioural test boundaries before deciding.
Concurrency And State ❓ Inconclusive Placeholder only; evidence collection is still in progress. Inspect the regeneration tests and shared metrics state before deciding.
Architectural Complexity And Maintainability ❓ Inconclusive Evidence gathering is incomplete; no verdict submitted as a final assessment. Inspect the parent implementation, dependency graph, and tests before deciding whether the new module split reduces complexity.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title describes the main lockfile regeneration change and includes the required (regenerate-lockfiles) ExecPlan reference.
Description check ✅ Passed The description directly explains the lockfile discovery change, supporting updates, implementation scope, and validation evidence.
Docstring Coverage ✅ Passed Docstring coverage is 85.53% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Testing (Overall) ✅ Passed Mark as PASS: merge/path property tests, regeneration failure/metric tests, and an actual CLI BDD scenario with exact root and discovered Cargo calls guard the changed behaviour.
Developer Documentation ✅ Passed The developer guide documents the new lockfile modules, repository boundary, discovery, dry-run behaviour, failure handling, tooling pins, and metrics; the design doc and completed ExecPlan also re...
Module-Level Documentation ✅ Passed All 18 changed Python modules have module-level docstrings; new lockfile modules describe their purpose and component boundaries, and lockfile.py documents publish/bump relationships.
Testing (Property / Proof) ✅ Passed Mark this check PASS: Hypothesis properties exercise merge ordering/deduplication, path safety, and all-manifest failure aggregation; no new formal proof assumption needs exhaustive proof.
Testing (Compile-Time / Ui) ✅ Passed Pass this check: the repository has no Rust or TypeScript sources, so trybuild is not applicable; focused Syrupy snapshots and semantic assertions cover bump, stale-lock, and regeneration output.
Domain Architecture ✅ Passed Bump and publish workflows depend on repository protocols, while Cargo/git runners, path handling, discovery, and regeneration stay behind injected adapters; CLI wiring supplies the runner.
Observability ✅ Passed The lockfile change adds bounded regeneration and discovery metrics, elapsed-time metrics, and logs at discovery, per-manifest, aggregate-failure, and completion boundaries.
Security And Privacy ✅ Passed No secrets or new credential handling found. Git and Cargo commands use argument sequences with shell=False; manifest paths are resolved and workspace-bound, and metrics use bounded labels.
Performance And Resource Use ✅ Passed Approve: the change adds one Git query, linear filtering, one O(n log n) deterministic sort, and one intentional Cargo call per required manifest; no quadratic walk, retry loop, or async blocking p...
Rust Compiler Lint Integrity ✅ Passed The PR changes no Rust files; the repository has no .rs files, Rust lint suppressions, or Rust clone calls, and Cargo.toml is unchanged.
✨ 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 regenerate-lockfiles

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

codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review July 8, 2026 09:48
chatgpt-codex-connector[bot]

This comment was marked as resolved.

codescene-access[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the regenerate-lockfiles branch from fbc0a6d to 2509734 Compare July 9, 2026 09:09
codescene-access[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the regenerate-lockfiles branch from 2509734 to 1198e46 Compare July 13, 2026 19:29
codescene-access[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot added the Roadmap label Jul 13, 2026
coderabbitai[bot]

This comment was marked as resolved.

codescene-access[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the regenerate-lockfiles branch from 84f36e7 to 27ca243 Compare July 13, 2026 22:29
codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@leynos

leynos commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Have the following failed checks now been resolved?

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

Do not treat warnings as optional or aspirational. Where a change is out of scope for this PR, propose a GitHub issue unless one exists already. (Treat documentation and validation coverage as in scope).

❌ Failed checks (4 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Developer Documentation ⚠️ Warning docs/developers-guide.md still has a malformed lading.commands.lockfile .discover_tracked_lockfiles ref, and the execplan still leaves ty pinning and dry-run wording stale. Fix the module path, update the execplan to match implemented live/dry-run behaviour, and remove or mark the ty pinning follow-up as complete.
Testing (Property / Proof) ⚠️ Warning merge_discovered_manifests adds order/dedup invariants, but only example tests were added; repo guidance calls for Hypothesis on such invariants. Add a Hypothesis test for merge_discovered_manifests covering arbitrary configured/discovered orders, resolved-path de-duplication, and the non-git fallback.

@coderabbitai

This comment was marked as resolved.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@lodyai
lodyai Bot force-pushed the regenerate-lockfiles branch from 58bb2a7 to 37be73c Compare July 27, 2026 12:55
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lading/commands/bump_lockfile_regeneration.py`:
- Around line 183-189: Update _regeneration_failure_cause to use structural
pattern matching on error.__cause__ instead of the current isinstance chain,
preserving the existing CommandSpawnError precedence, ValueError mapping, and
cargo_exit fallback.

In `@tests/unit/test_bump_lockfile_regeneration_metrics.py`:
- Around line 107-125: The parametrized test
test_regenerate_lockfiles_records_failure_cause currently omits the runner_value
failure branch. Add a runner helper that raises ValueError and include it with
expected_cause set to runner_value, ensuring _regeneration_failure_cause() is
verified for this operational failure.
🪄 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: 31d3af26-7bb7-425d-a779-539c97e818d0

📥 Commits

Reviewing files that changed from the base of the PR and between 19b3479 and 4b0b785.

📒 Files selected for processing (9)
  • docs/developers-guide.md
  • lading/commands/bump_lockfile_manifests.py
  • lading/commands/bump_lockfile_regeneration.py
  • lading/commands/bump_lockfiles.py
  • lading/commands/lockfile.py
  • tests/unit/test_bump_lockfile_manifest_merge.py
  • tests/unit/test_bump_lockfile_rebuild.py
  • tests/unit/test_bump_lockfile_regeneration_metrics.py
  • tests/unit/test_lockfile.py
🔗 Linked repositories identified

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

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

Comment thread lading/commands/bump_lockfile_regeneration.py Outdated
Comment thread tests/unit/test_bump_lockfile_regeneration_metrics.py
Use structural matching for bounded regeneration failure causes and cover
runner value failures. Synchronize the completed ExecPlan with the final
module sizes, public interfaces, observability contracts, and portable
repository path.
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/execplans/regenerate-lockfiles.md`:
- Around line 346-351: Update the documentation sentence describing
discover_tracked_lockfiles to say “In a non-Git directory, it logs”, preserving
the existing behavior description and applying the required en-GB-oxendict
grammar.

In `@tests/unit/test_bump_lockfile_regeneration_metrics.py`:
- Around line 78-91: Update every bare assert at
tests/unit/test_bump_lockfile_regeneration_metrics.py:78-91, 108-115, and
179-194, plus tests/unit/test_lockfile.py:663-665, to use assert …, "focused
diagnostic message". Add messages for the returned lockfiles, success/duration
checks, expected failure-cause counter, partial-success and Cargo-exit counters,
discovered paths, suppressed counter, and suppressed informational log; preserve
all existing semantic checks.
🪄 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: 9474ed13-45aa-442c-92d4-eebb6371b3d0

📥 Commits

Reviewing files that changed from the base of the PR and between 19b3479 and fd3e38c.

📒 Files selected for processing (10)
  • docs/developers-guide.md
  • docs/execplans/regenerate-lockfiles.md
  • lading/commands/bump_lockfile_manifests.py
  • lading/commands/bump_lockfile_regeneration.py
  • lading/commands/bump_lockfiles.py
  • lading/commands/lockfile.py
  • tests/unit/test_bump_lockfile_manifest_merge.py
  • tests/unit/test_bump_lockfile_rebuild.py
  • tests/unit/test_bump_lockfile_regeneration_metrics.py
  • tests/unit/test_lockfile.py
🔗 Linked repositories identified

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

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

Comment thread docs/execplans/regenerate-lockfiles.md Outdated
Comment thread tests/unit/test_bump_lockfile_regeneration_metrics.py Outdated
Add focused assertion messages for lockfile observability contracts and
correct the completed ExecPlan grammar. Synchronize Ruff 0.15.21 across the
project gates and adjust the property summaries required by that version.
codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access 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.

No quality gates enabled for this code.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 1, 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.

@leynos

leynos commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

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

User-Facing Documentation ⚠️ Warning The user guide clearly documents lockfile discovery, dry-run behaviour, configuration, failures, and stale-lock repairs, but no n+1 migration document signposts this new behaviour for the 0.2.0 pre... Add a 0.3.0 migration document or migration section that explains tracked-lockfile discovery, dry-run output, configuration fallback, and the regeneration escape hatch.

@coderabbitai

coderabbitai Bot commented Aug 1, 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

echo '=== Matching open issues ==='
gh issue list \
  --repo leynos/lading \
  --state open \
  --limit 100 \
  --search 'migration 0.3.0 lockfile discovery dry-run' \
  --json number,title,url,body \
  --jq '.[] | {number,title,url,body}' || true

echo '=== PR metadata ==='
gh pr view 160 \
  --repo leynos/lading \
  --json url,title \
  --jq '{url,title}'

echo '=== Requesting comment permalink ==='
gh api 'repos/leynos/lading/issues/160/comments?per_page=100' \
  --jq '.[] | select(.user.login == "leynos") | select(.body | contains("please create a new GitHub issue for the following")) | {url: .html_url, body: .body}' || true

Length of output: 1626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

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

Add a 0.3.0 migration document or a 0.3.0 migration section for the lockfile regeneration changes delivered by PR `#160`.

## Rationale

The user guide documents the behaviour in detail. Users upgrading to the next release need a migration entry that signposts the changed lockfile behaviour and the available recovery path.

## Affected areas

- Add the migration document or section in the repository's release or migration documentation.
- Link to the existing user-guide material when that avoids duplicated operational detail.

## Required content

Document all of the following:

1. `lading bump` discovers Git-tracked `Cargo.lock` files and includes their adjacent `Cargo.toml` manifests.
2. Dry-run discovers and reports the same lockfile set without modifying files.
3. In a non-Git workspace, discovery falls back to `bump.lockfile_manifests` and emits a warning.
4. `bump.lockfile_manifests` extends coverage for untracked lockfiles and nested lockfiles in non-Git workspaces.
5. `lading bump --no-rebuild-lockfiles` is the regeneration escape hatch. Explain that users must regenerate stale lockfiles before publishing when they use it or modify manifests directly.

## Acceptance criteria

- Add a 0.3.0 migration document or migration section.
- Explain tracked-lockfile discovery, dry-run output, configuration fallback, and the regeneration escape hatch.
- Keep the content consistent with `docs/users-guide.md`.
- Add navigation or an index link when the documentation structure requires one.
- Validate documentation formatting and links with the repository's documented validation targets.

## Backlinks

- Source PR: https://github.com/leynos/lading/pull/160
- Source comment: https://github.com/leynos/lading/pull/160#issuecomment-5150893389
- Requested by: `@leynos`
EOF
)

gh issue create \
  --repo leynos/lading \
  --title 'Document 0.3.0 migration for lockfile discovery and regeneration' \
  --assignee leynos \
  --body "$body"

Length of output: 2223


@leynos Created and assigned #219 — Document 0.3.0 migration for lockfile discovery and regeneration.

The issue includes the required migration content, acceptance criteria, and backlinks to PR #160 and your request.

You are interacting with an AI system.

@leynos
leynos merged commit 50b61f0 into main Aug 1, 2026
7 checks passed
@leynos
leynos deleted the regenerate-lockfiles branch August 1, 2026 09:50
lodyai Bot pushed a commit that referenced this pull request Aug 1, 2026
Repair artefacts from rebasing the DOC docstring work onto main's
lockfile refactor (#160):

- restore the WorkspaceGraph and CallTrackingRunner class docstrings that
  the entity merge dropped;
- drop the "Returns: None" blocks that DOC202 rejects on None-returning
  steps and fixtures, keeping the informative prose as Notes;
- suppress DOC502 at the single propagated-raise site in the new
  bump_lockfile_paths.resolve_lockfile_paths, using a function-level noqa
  rather than a file-wide exemption;
- re-apply the partial-update wording fix in its new home: the
  single-versus-aggregated decision keys on how many manifests were
  attempted, matching _raise_aggregated_failure's len(manifests) branch,
  not on how many lockfiles were regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lodyai Bot pushed a commit that referenced this pull request Aug 1, 2026
discover_tracked_lockfiles hid a non-git workspace behind a warning
and a silent empty tuple, so callers could not distinguish "no
tracked lockfiles" from "discovery never ran". Filesystem access was
also mixed into the function rather than confined to a port.

Raise a typed NotAGitRepositoryError (subclass of
LockfileDiscoveryError) for non-git workspaces and keep
LockfileDiscoveryError for other git failures. The skip policy moves
to the caller: publish pre-flight catches NotAGitRepositoryError,
warns, and continues, preserving existing operator behaviour while
making the condition explicit at the API.

Filesystem access is now documented as confined to the injected
manifest_exists port and git access to the injected runner; the
function performs no direct I/O of its own.

Replace the silent-skip unit test with a typed-exception assertion,
add a caller-policy test for the pre-flight skip, and add integration
tests that exercise discovery against real git repositories in
temporary directories through the real subprocess runner (tracked
versus untracked lockfiles, target/ exclusion, manifest adjacency,
and the non-git error).

Extract the lockfile freshness policy (_validate_lockfile_freshness,
_collect_stale_lockfiles, _build_stale_lockfile_message) into the
colocated module publish_lockfile_preflight. Adding the skip branch
took publish_preflight past the repository's 400-line file limit, and
the freshness policy is a coherent unit: it depends only on the
LockfileInspectionRepository port, while publish_preflight retains the
cargo and git command orchestration and stays the composition root
that binds the adapter.

Apply the same caller-owned skip policy on the bump side. Since #160,
bump discovers tracked lockfiles through
bump_lockfile_manifests.merge_discovered_manifests, whose documented
contract is that a non-git workspace returns the configured manifests
unchanged. That relied on discovery's silent empty tuple, so the typed
error would otherwise abort `lading bump` outside git control; the
merge helper now catches NotAGitRepositoryError, warns, and returns the
configured tuple.

Closes #79
leynos added a commit that referenced this pull request Aug 2, 2026
* Truncate private helper docstrings (#162)

Keep private helper documentation to the required single-line summaries
and remove structured sections intended only for public interfaces.

* Normalize public bump docstrings (#162)

Document the public TOML update helpers with complete NumPy-style
parameter and return sections.

* Normalize preflight fixture docstring (#162)

Convert the remaining mixed Google and NumPy-style fixture docstring to
the repository's structured public-interface format.

* Add runnable docstring examples (#162)

Show representative update results for the public TOML helpers and the
preflight fixture's compiletest dependency override.

* Enforce DOC docstring rules across the codebase

Add the pydoclint `DOC` rule group to the Ruff lint selection so that
docstrings must document returns, yields, and directly-raised exceptions,
complementing the existing NumPy `D` convention and preview mode already
configured.

Enabling the rule surfaced missing sections throughout `lading/` and the
test suite. Add NumPy-style `Returns`, `Raises`, and `Yields` sections to
the affected functions, fixtures, and helpers, and remove the handful of
extraneous `Returns`/`Raises` entries that documented exceptions raised only
by delegated calls. Docstrings and the single config line are the only
changes; no runtime logic was altered.

Relates to #162.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Scope DOC completeness rules to public interfaces

Enable ``ignore-one-line-docstrings`` for pydoclint so the DOC rule
group enforces Returns/Raises/Yields completeness on multi-line public
docstrings while exempting the concise single-line summaries the project
convention uses for private helpers. This reconciles DOC enforcement
with the established private-helper docstring style.

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

* Apply docstring review feedback across the tree

Action CodeRabbit review findings for the DOC docstring work:
collapse private-helper docstrings to concise single-line summaries
(now DOC-exempt via ignore-one-line-docstrings), complete public
interface contracts with typed Parameters/Returns/Examples, and correct
Raises/Returns wording. Findings that would violate the enforced DOC
rules (renaming factory raise-callables to concrete types, documenting
delegated/propagated raises) are intentionally not applied. Also fix
counter_value to default missing keys to zero, and refactor duplicated
E2E/BDD step setup and invocation-recording helpers.

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

* Apply second round of docstring review feedback

Action the follow-up CodeRabbit findings: clarify lockfile empty-tuple
docs, add runnable public Examples (cli, config, cmd_mox_runner),
complete/collapse docstrings per the public/private convention, remove
the create_nontrivial_workspace Examples block (CodeScene Large Method),
and consolidate redundant unit-test fixture aliases. Findings that would
violate the enforced DOC rules (propagated-exception Raises on
transpose_readme_to_crate/optional_mapping/subprocess helpers, factory
raise-callable renames) are intentionally skipped.

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

* Trim publish.run docstring Examples for CodeScene

Remove only the Examples section from the public ``run`` docstring to
resolve CodeScene's "Large Method" diagnostic. The summary, Parameters,
and Returns sections are unchanged, and no production logic, signature,
delegation boundary (``_dispatch_publication``), or test is touched.

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

* Extract fixture-crate creation from create_nontrivial_workspace

Move the core/utils/app crate-creation block into a new private helper
`_create_fixture_crates`, returning the manifest-path mapping, to resolve
CodeScene's Large Method diagnostic through a meaningful extraction.
Behaviour, dependency fixture text, crate names, and the returned
NonTrivialWorkspace are unchanged; the public docstring is untouched.

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

* Document the LadingError contract in TOML-coercion Raises

Replace the misleading `_reject` entry in the NumPy-style Raises headings
of expect_mapping, string_mapping, expect_string, boolean, and
non_negative_int with `LadingError` — the base of the caller-injected
`error` subclass that `_reject` constructs for callers to raise. Each
validation-specific failure description is preserved, and the private
`_validate_string_pair` helper keeps its one-line docstring.

pydoclint (DOC501/DOC502) keys the Raises type token on the syntactic
raise-callable (`_reject`) and cannot resolve it to the real exception,
so a scoped per-file-ignore for those two rules on the two coercion
modules lets the docstrings state the accurate contract, mirroring the
existing `ignore-one-line-docstrings` DOC reconciliation.

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

* Document _prepare_cmd_mox_context Returns and Raises

Expand the private helper's docstring with NumPy-style Returns and
Raises sections: it returns the validated IPC timeout in seconds and
raises CmdMoxError when CMOX_IPC_SOCKET is unset (raised directly) or
when CMOX_IPC_TIMEOUT is unparseable, non-finite, or non-positive
(propagated from the reachable _resolve_cmd_mox_timeout call).
Documentation-only; CmdMoxError is raised directly in the body, so the
DOC rules pass without suppression.

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

* Complete make_plan_factory fixture docstring

Add a NumPy-style Parameters section documenting the make_crate
fixture/factory and its role, clarify the Returns section describing the
returned plan-building callable, and add a runnable Examples section that
invokes the returned factory (via __wrapped__ with a lightweight stand-in)
and shows the resulting plan's publishable crate names. Signature and
behaviour are unchanged; documentation-only.

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

* Document _process_readme_transposition Returns

Add a NumPy-style Returns section for the returned set[Path]: the crate
README paths that were written, or reported as would be written during a
dry run. Expanding the docstring surfaces the direct re-raise of
ReadmeTranspositionError (bare `raise` in the except block), so a matching
Raises section is added to satisfy DOC501. Documentation-only; signature
and runtime behaviour unchanged.

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

* Document E2E helper contracts and true exception type

Add NumPy-style Parameters sections to the E2EExpectationError factory
methods (unsupported_fixture_version, dependency_entry_not_string,
args_prefix_mismatch) and to run_cli, extract_dependency_requirement,
find_staging_root, and filter_records; add Parameters to
given_cargo_commands_stubbed. In extract_dependency_requirement,
find_staging_root, and given_nontrivial_workspace_in_git_repo, name the
raised type as E2EExpectationError (constructed by the relevant factory
classmethod), keeping the factory methods documented as factories.

Those callers raise via factory classmethods, so pydoclint keys
DOC501/DOC502 on the method name; a scoped per-file-ignore for the two
E2E files lets the docstrings state the true E2EExpectationError contract,
mirroring the toml_coerce reconciliation. Documentation-only; signatures
and runtime behaviour unchanged.

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

* Document propagated exception contracts

Add NumPy-style Raises sections documenting the exceptions callers must
handle: subprocess_runner (ValueError, CommandSpawnError),
invoke_via_subprocess (CommandSpawnError), write_to_sink (OSError,
ValueError), transpose_readme_to_crate (ReadmeTranspositionError), and
optional_mapping (_reject factory).

These functions propagate their exceptions from delegated helpers rather
than raising directly, so pydoclint (DOC501/DOC502) — which only tracks
direct raises — cannot express the contract. Scope those two rules off
for subprocess_runner.py and bump_readme.py (optional_mapping's module
already carries the ignore). Documentation-only; behaviour unchanged.

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

* Document the true exception types in sequence/metadata Raises

Replace the factory raise-callable names in NumPy-style Raises headings
with the real exception classes: expect_sequence, validate_string_sequence,
string_tuple, _validate_matrix_entry, and string_matrix now document
LadingError (the base of the injected `error` subclass that `_reject`
constructs and the function raises), and _parse_cargo_metadata documents
CargoMetadataParseError while preserving the invalid-JSON and
non-object-payload conditions.

pydoclint keys DOC501/DOC502 on the syntactic raise-callable (`_reject`,
the CargoMetadataParseError factory classmethods) and cannot resolve them
to the real exception, so a scoped per-file-ignore on the two modules lets
the docstrings state the true contract, mirroring the earlier toml_coerce
reconciliation. Documentation-only; runtime behaviour unchanged.

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

* Align docstrings with the updated AGENTS.md guidance

Action the third round of review findings under the new docstring
convention (AGENTS.md, commit e731d1f): public APIs get comprehensive
NumPy-style documentation with runnable examples where appropriate, and
private helpers collapse to concise single-line summaries, keeping
structured sections only where they document non-obvious behaviour.

Public surfaces: typed Parameters and runnable Examples across the
toml_coerce coercion helpers, runner.coerce_text, config.use_configuration,
the E2E helper factories, and the workspace-metadata steps; corrected the
cli bump/publish examples to assert stable substrings of the delegated
string contracts rather than outdated literals; documented the publish
workflow's propagated exception contract; and fixed bump.run's Returns to
cover dry-run previews.

Private helpers: collapsed graph_build, toml_coerce, bump, cmd_mox, and
test-helper docstrings to single lines, retaining the CrossHair contracts
in bump_output and the typed-wrapper rationale in _coercion where the
prose documents a genuine local constraint. Removed test Examples that
only restated test logic, per the updated test-documentation rule.

Scoped DOC per-file-ignores cover the modules whose findings pydoclint
cannot express: propagated exceptions (it tracks only direct raises) and
bump_output's CrossHair contracts, which keep private docstrings
multi-line where the convention omits Returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Trim plan_publication docstring Examples for CodeScene

Remove only the Examples section from plan_publication's docstring to
resolve CodeScene's "Large Method" diagnostic. The summary and the
Parameters, Returns, and Raises sections are unchanged, and no
executable logic is touched: the coordinator flow still delegates to
_categorize_crates, _resolve_configured_order, and
_resolve_topological_order, with the same signature, PublishPlan
construction, and exception behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Correct and complete docstring contracts

Fix inaccurate documented contracts and fill gaps found in review:

- bump.run: the dry-run example showed a fabricated summary; use the real
  "Dry run; would update version to ..." prefix that _format_header emits.
- publish.run: packaging failures raise PublishPreflightError, not
  PublishError, and PublishPreparationError also covers staged-manifest
  patch preparation.
- cmd_mox_runner: add a structured Raises section (CmdMoxError, ValueError)
  and trim the now-redundant Notes prose; collapse the private
  _prepare_cmd_mox_context to a single-line summary.
- config: document use_configuration's parameter and add a runnable
  current_configuration example.
- metadata: collapse three private helpers, add runnable examples to both
  CargoMetadataParseError factories, and complete load_cargo_metadata with
  typed parameters, an example, and its full propagated Raises set.
- subprocess_runner: type every public Parameters entry and add examples;
  relay_stream now states in prose that it returns None and appends to
  buffer (DOC202 forbids a Returns section there).
- toml_coerce: note that _validate_string_pair rejects non-string keys and
  values, document is_non_empty_sequence, collapse _validate_matrix_entry.
- bdd steps: examples now exercise the documented step functions and define
  the names they reference.

Documentation-only: the AST of every changed file is unchanged once
docstrings are stripped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Document step failure modes and tidy fixture examples

Add Raises sections to the publish package-ordering, dry-run, and
live-publish steps covering both the missing-invocation failure from
_get_required_invocations and the downstream ordering and flag
assertions, suppressing DOC502 per function since those raises are
propagated. Extend the five cargo::test pre-flight steps' existing
Raises wording to name the missing-invocation cause too.

Rewrite the six publish conftest doctests to use TemporaryDirectory
context managers instead of tempfile.mkdtemp(), keeping each example's
operations inside the context so the directory is removed automatically.

Documentation-only; every changed file's AST is unchanged once
docstrings are stripped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Reconcile docstring work with the lockfile module split

Repair artefacts from rebasing the DOC docstring work onto main's
lockfile refactor (#160):

- restore the WorkspaceGraph and CallTrackingRunner class docstrings that
  the entity merge dropped;
- drop the "Returns: None" blocks that DOC202 rejects on None-returning
  steps and fixtures, keeping the informative prose as Notes;
- suppress DOC502 at the single propagated-raise site in the new
  bump_lockfile_paths.resolve_lockfile_paths, using a function-level noqa
  rather than a file-wide exemption;
- re-apply the partial-update wording fix in its new home: the
  single-versus-aggregated decision keys on how many manifests were
  attempted, matching _raise_aggregated_failure's len(manifests) branch,
  not on how many lockfiles were regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Restore WorkspaceGraph fields dropped by the entity merge

The rebase onto main's lockfile refactor silently dropped the
workspace_root and crates field declarations from WorkspaceGraph along
with its class docstring, so every attribute access failed typecheck
with "Object of type WorkspaceGraph has no attribute crates". Restore
both fields.

An AST audit across all 65 changed files confirms no other class lost
fields and no functions or classes were lost, beyond this branch's two
intentional removals (the four specialised invocation getters replaced
by _get_required_invocations, and the redundant fixture aliases).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Complete exception contracts and correct docstring inaccuracies

Document the exceptions callers must handle, and fix documentation that
disagreed with the code:

- config: add ConfigurationError Raises to the five from_mapping
  classmethods and load_configuration, extend load_from_loader's entry to
  cover validation failures, and inline the single-use loader local per
  .rules/python-return.md R504.
- cmd_mox_runner: document CommandSpawnError propagated from passthrough
  spawning.
- load_workspace: document the four cargo-metadata errors and
  WorkspaceModelError it propagates.
- metrics.duration_stats: the empty-stats fallback applies when the
  (name, labels) series is unrecorded, not when the name is absent.
- bump_lockfile_regeneration: the Raises text now keys on how many
  manifests were attempted, matching len(manifests) == 1.
- _update_manifest: the summary now covers dependency sections too.
- lockfile, publish_plan.render_section, subprocess_runner: type the
  Parameters entries; widen normalise_environment to Mapping[str, object]
  since it stringifies every value (its own doctest already passed an int).
- toml_coerce doctests now use LadingError rather than a built-in
  ValueError, matching _ErrorType and the Raises documentation.
- bdd steps: drop the unrunnable then_dependency_requirement example,
  document shlex.split's ValueError, record the first-override-wins and
  --allow-dirty normalisation rules, demonstrate tmp_path by equality
  rather than a fabricated PosixPath, and type cli_run as _BumpCliRun.

Every DOC502 suppression now carries an inline justification naming the
propagation path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Align the subprocess env contract and apply the one-line helper convention

- subprocess_runner: widen SubprocessContext.env to Mapping[str, object],
  matching normalise_environment, and widen the two consumers that read it
  (_log_subprocess_environment, _redact_environment) so the boundary stays
  type-consistent. _redact_environment already stringified every value, so
  only the annotations were stale. Refresh the normalise_environment comment
  that described non-string values as arriving "despite the annotated
  signature", which the widened signature no longer contradicts.
- config: every DOC502 suppression now carries an inline justification naming
  its propagation path -- the five from_mapping classmethods raise through the
  shared mapping validators, and load_configuration delegates to
  load_from_loader.
- bdd steps: collapse _normalise_preflight_responses and
  when_invoke_lading_publish to single-line summaries, per the
  ignore-one-line-docstrings convention for private helpers. The two
  non-obvious rules the former documented (first publish override wins,
  --allow-dirty normalised to config.allow_dirty) move to inline comments at
  the branches that implement them, so no behaviour detail is lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Key the lockfile failure-mode docs on manifests attempted

_raise_aggregated_failure branches on len(manifests) == 1, so describing the
re-raise path in terms of how many lockfiles were regenerated is too narrow --
and doubly wrong, since a failed manifest produces no regenerated lockfile at
all. Every manifest is attempted, so the distinction is the attempt count.

The regenerate_lockfiles docstring was corrected in 5af1852; this applies the
same correction to the three surviving prose copies in the user and developer
guides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Widen the subprocess_runner env parameter to match the boundary

9fe366b widened SubprocessContext.env and its consumers to Mapping[str,
object], but left subprocess_runner's own env parameter as Mapping[str, str].
That made the public entry point the narrowest link in the chain: a caller
could not pass a Path or int through subprocess_runner even though the context
field, normalise_environment and the redacting logger all accept one.

Widening a parameter is contravariance-safe, so subprocess_runner still
satisfies the CommandRunner protocol; ty confirms this. The protocol itself is
left alone -- narrowing that port would cascade to every implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Apply the one-line convention to two workspace coercion helpers

_expect_sequence: the discarded paragraph explained why the helper is a typed
wrapper rather than functools.partial. That is design rationale rather than
caller-facing behaviour, and the return-narrowing it describes is already
documented on the two overload stubs, which are the caller-visible contract.
The overloads and their docstrings are untouched.

_coerce_publish_setting: its prose documented a coercion rule that is not
recoverable from the (value: object, package_id: str) -> bool signature --
None and non-empty registry lists mean publishable, false and an empty list do
not. AGENTS.md requires non-obvious behaviour to survive, so the replacement
one-liner carries the rule itself rather than restating the function name.

Both are now exactly one physical line, so ignore-one-line-docstrings exempts
them from DOC201/DOC501 despite dropping Returns and Raises.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Share one typed contract for BDD CLI run results

Introduce CliRunResult, a TypedDict with the four keys _run_cli actually
produces, and use it wherever a publish step passes that dict around.

The contract lives in its own module rather than in test_common_steps, which
owns _run_cli. test_common_steps imports the bump and publish step modules at
the bottom of the file so their steps register with pytest-bdd, which makes it
a hub: test_common_steps -> test_publish_when_steps ->
test_publish_infrastructure -> test_common_steps would close a real cycle.
That is why _run_cli is reached through TYPE_CHECKING guards and deferred
function-body imports today. cli_run_types imports no sibling step module, so
it cannot join that cycle. pytest collects only test_*.py, so the module is
not mistaken for a test, and the tests/bdd/steps/*.py per-file ignores still
cover it.

test_bump_steps already declared _BumpCliRun with these exact four fields, so
the shared contract replaces it rather than sitting beside it. Typing _run_cli
also makes the typ.cast in _invoke_lading_bump redundant, so it becomes a
direct return.

Every import of the contract sits in a TYPE_CHECKING block: the annotations
are deferred by __future__.annotations and the type has no runtime use, so
ruff's TC001 requires it there.

The e2e run_cli helper is left alone -- it returns workspace_root plus an
extra command key, so it does not fit this contract. The shared then_* steps
in test_common_steps and test_bump_steps keep dict[str, typ.Any]; they are not
publish-specific, and pytest-bdd injects them as fixtures, so no call edge
forces the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Type the end-to-end CLI result contract

run_cli builds one dict literal with five unconditional keys, so declare
_CliRunResult alongside it and return that instead of dict[str, typ.Any].
The shape stays exactly as it was: command is a list[str] and workspace_root
is the Path passed through unconverted.

This is a separate contract from the BDD tree's CliRunResult rather than a
shared one: the e2e result carries an extra command key and names the path
workspace_root, so the two cannot be unified without changing one of the
dictionaries.

Retype the consumers that actually hold a CLI result -- the private
_run_lading_in_e2e_workspace helper, the three when_run_lading_* steps that
target the cli_run fixture, and the two then steps that read it -- plus the
Returns sections that named the old type. The e2e_state and publish_spies
mappings keep dict[str, typ.Any]; they are unrelated bags, and e2e_state in
particular gains a regenerated_marker key at runtime that a closed TypedDict
would reject.

_CliRunResult is imported under TYPE_CHECKING because test_e2e_steps only
names it in annotations; ruff's TC001 requires that placement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Inline the two single-use CargoMetadataParseError factories

CodeScene flagged invalid_json and non_object_payload as duplicated structure.
The duplication was real but the shared part was scaffolding, not behaviour:
each body was one return cls("<literal>"), and each was called exactly once,
on adjacent lines of _parse_cargo_metadata. Extracting a shared abstraction
would only have produced cls(message) -- the constructor that already exists.

Inlining removes 30 lines and makes the module consistent with itself: every
other raise here already constructs the error directly, so the factories were
the outlier rather than the pattern. The class is not re-exported and nothing
outside the two call sites referenced either factory.

This also retires a file-wide lint suppression. The DOC501/DOC502 ignore for
metadata.py existed because pydoclint keys those rules on the syntactic
raise-callable and could not resolve a factory classmethod to its exception
type. With the raises direct, only load_cargo_metadata still needs DOC502 --
for exceptions it genuinely propagates -- so a justified per-function noqa
replaces the file-wide entry, and the pyproject comment no longer describes
factories that have been deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: leynos <leynos@rohga>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: leynos <leynos@troubledskies.net>
leynos added a commit that referenced this pull request Aug 14, 2026
discover_tracked_lockfiles hid a non-git workspace behind a warning
and a silent empty tuple, so callers could not distinguish "no
tracked lockfiles" from "discovery never ran". Filesystem access was
also mixed into the function rather than confined to a port.

Raise a typed NotAGitRepositoryError (subclass of
LockfileDiscoveryError) for non-git workspaces and keep
LockfileDiscoveryError for other git failures. The skip policy moves
to the caller: publish pre-flight catches NotAGitRepositoryError,
warns, and continues, preserving existing operator behaviour while
making the condition explicit at the API.

Filesystem access is now documented as confined to the injected
manifest_exists port and git access to the injected runner; the
function performs no direct I/O of its own.

Replace the silent-skip unit test with a typed-exception assertion,
add a caller-policy test for the pre-flight skip, and add integration
tests that exercise discovery against real git repositories in
temporary directories through the real subprocess runner (tracked
versus untracked lockfiles, target/ exclusion, manifest adjacency,
and the non-git error).

Extract the lockfile freshness policy (_validate_lockfile_freshness,
_collect_stale_lockfiles, _build_stale_lockfile_message) into the
colocated module publish_lockfile_preflight. Adding the skip branch
took publish_preflight past the repository's 400-line file limit, and
the freshness policy is a coherent unit: it depends only on the
LockfileInspectionRepository port, while publish_preflight retains the
cargo and git command orchestration and stays the composition root
that binds the adapter.

Apply the same caller-owned skip policy on the bump side. Since #160,
bump discovers tracked lockfiles through
bump_lockfile_manifests.merge_discovered_manifests, whose documented
contract is that a non-git workspace returns the configured manifests
unchanged. That relied on discovery's silent empty tuple, so the typed
error would otherwise abort `lading bump` outside git control; the
merge helper now catches NotAGitRepositoryError, warns, and returns the
configured tuple.

Address review feedback. NotAGitRepositoryError now carries the failing
workspace_root as a structured attribute, matching CommandSpawnError and
WorkspaceDependencyCycleError, so callers need not parse the message. Add
a bounded lockfile.discovery.failed counter (reason=not_git|git_error) and
log the unexpected-failure branch at the failure boundary; success volume
stays on lockfile.discovered so quiet runs stay quiet.

Discovery classifies a non-git workspace by matching git's English text,
which a localized machine would translate and silently misclassify. Pin
the C locale in subprocess_runner, the single adapter that spawns
processes, via the new lading.utils.process.c_locale_env helper. Test
doubles bypass it, so no double needs widening, and cargo's output is
covered too. Classifying on the exit status instead is not possible:
git ls-files exits 128 for every fatal condition.

Move the LockfileInspectionRepository port and its adapter into
lockfile_repository, with their tests, keeping lockfile.py inside the
400-line limit and separating the hexagonal boundary from the domain
operations.

Restore the inline-code exclusion in typos.local.toml. Regenerating
typos.toml from the shared authority had dropped it, so the spelling gate
flagged identifiers such as `normalise_workspace_root` that must keep
their source spelling.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants