From 08dba6c565c9ecb04e08f027af16325051aab697 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sun, 25 Oct 2026 20:16:52 +0100 Subject: [PATCH] fix(application): diagnose disagreeing managed version sources explicitly (ticket-216) - A coordinated governed release lands its version projection across workstream tickets; between merges the managed declarations disagree and goal -a answered generically (or with the confusing clean force-publish error). - The pull-request boundary now names the exact values and the remaining coordinated release tickets. - Tests: 17 in the completion module (1 new); full suite green; governance check PASS. --- goal/push/core.py | 23 ++++++++ project/TICKETS.md | 1 + project/ticket-216/README.md | 26 ++++++++ project/ticket-216/intent.json | 82 ++++++++++++++++++++++++++ tests/test_push_workflow_completion.py | 45 ++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 project/ticket-216/README.md create mode 100644 project/ticket-216/intent.json diff --git a/goal/push/core.py b/goal/push/core.py index d7f81f1..e4362a1 100644 --- a/goal/push/core.py +++ b/goal/push/core.py @@ -435,6 +435,17 @@ def _has_committed_unreleased_sources() -> bool: return _registry_publication_pending(project_types) +def _managed_version_values() -> List[str]: + """Return the distinct managed version declarations in the tree.""" + try: + from goal.cli.version_state import collect_version_sources + + sources = collect_version_sources() + except (ImportError, OSError, ValueError): + return [] + return sorted({source.value for source in sources if source.value is not None}) + + def _registry_publication_pending(project_types: List[str]) -> bool: """Return whether the tree version is newer than the published registry.""" if not project_types: @@ -763,6 +774,18 @@ def execute_push_workflow( if delivery is not None: ticket = resolve_pull_request_ticket(delivery, ticket) if delivery.mode == "pull-request" and ticket is None: + disagreeing = _managed_version_values() + if len(disagreeing) > 1: + # A coordinated release lands its version projection across + # workstream tickets; between them the managed declarations + # disagree. Name that exact state instead of a generic + # release-ticket or clean force-publish message. + raise click.ClickException( + "Managed version sources disagree (" + + ", ".join(disagreeing) + + "): land the remaining coordinated release tickets " + "(version files and runtime banner) before running goal -a." + ) if _has_committed_unreleased_sources(): if not _registry_publication_pending( list(detect_project_types_deep().keys()) diff --git a/project/TICKETS.md b/project/TICKETS.md index 552b079..659262c 100644 --- a/project/TICKETS.md +++ b/project/TICKETS.md @@ -425,4 +425,5 @@ This file indexes governance tickets without taking ownership of | **ticket-211** | [`README.md`](./ticket-211/README.md) | - | - | - | - | - | | **ticket-213** | [`README.md`](./ticket-213/README.md) | - | - | - | - | - | | **ticket-215** | [`README.md`](./ticket-215/README.md) | - | - | - | - | - | +| **ticket-216** | [`README.md`](./ticket-216/README.md) | - | - | - | - | - | diff --git a/project/ticket-216/README.md b/project/ticket-216/README.md new file mode 100644 index 0000000..70eddd7 --- /dev/null +++ b/project/ticket-216/README.md @@ -0,0 +1,26 @@ +# Ticket 216: Diagnose disagreeing managed version sources explicitly + +- **ID**: ticket-216 +- **Owner**: ai-claude +- **Status**: IN_PROGRESS +- **Workflow state**: EDIT +- **Created**: 2026-10-25 + +## Goal and scope + +A coordinated governed release lands its version projection across workstream +tickets (integration owns VERSION/pyproject, application owns the runtime +banner). Between those merges the managed declarations disagree; `goal -a` +answered with a generic "requires a release ticket" or the confusing clean +force-publish error. The pull-request boundary now names the exact state and +the remaining action. + +## Acceptance criteria + +- [ ] AC-01: Disagreeing managed versions raise "Managed version sources disagree (...)" naming the values and the remaining tickets. +- [ ] AC-02: Single-version behavior (pending completion, unreleased sources, no-change) is unchanged. + +## Tracking boundary + +This directory contains the minimal reviewed intent. Optional participant prose +and raw command logs are not required delivery output. diff --git a/project/ticket-216/intent.json b/project/ticket-216/intent.json new file mode 100644 index 0000000..bfc2ba1 --- /dev/null +++ b/project/ticket-216/intent.json @@ -0,0 +1,82 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-216", + "summary": "Diagnose disagreeing managed version sources explicitly", + "workstream": "application", + "classification": { + "kind": "BUG", + "priority": "P2", + "origin": "health" + }, + "allowedPaths": [ + "project/ticket-216/**", + "TODO.md", + "project/TICKETS.md", + "goal/push/core.py", + "tests/test_push_workflow_completion.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "ffb8cb135f8effe2d7129c0a8d9cdb708cafb0ee", + "targetBranch": "main", + "outcome": "The governed pull-request boundary reports disagreeing managed version sources explicitly, naming the values and the remaining coordinated release tickets instead of a generic message.", + "nonGoals": [ + "No change to version synchronization itself", + "No gate or policy changes" + ], + "complexity": "S", + "estimatedMinutes": 20, + "budgets": { + "maxImplementationFiles": 2, + "maxAffectedComponents": 1, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Add _managed_version_values() and guard the ticket-less pull-request boundary before the pending/no-change branches.", + "components": [ + { + "name": "push-workflow", + "paths": [ + "goal/push/core.py", + "tests/test_push_workflow_completion.py" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Protected revert of the two files; no data migration." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "python -m pytest -q tests/test_push_workflow_completion.py -k disagreeing" + ], + "evidence": "receipt:goal216/conflict-message" + }, + { + "criterion": "AC-02", + "commands": [ + "python -m pytest -q tests/test_push_workflow_completion.py", + "./project/governance-check.sh" + ], + "evidence": "receipt:goal216/regression" + } + ] + } +} diff --git a/tests/test_push_workflow_completion.py b/tests/test_push_workflow_completion.py index 52c7059..78b0cd6 100644 --- a/tests/test_push_workflow_completion.py +++ b/tests/test_push_workflow_completion.py @@ -68,6 +68,9 @@ def _run_workflow_until_project_detection(monkeypatch, ctx, recorded, sources_pe "goal.governance.delivery.resolve_pull_request_ticket", lambda policy, ticket, **kwargs: None, ) + monkeypatch.setattr( + push_core, "_managed_version_values", lambda: ["2.2.5"] + ) monkeypatch.setattr( push_core, "_has_committed_unreleased_sources", @@ -238,6 +241,45 @@ def test_is_governed_target_helper_stays_manifest_bound(tmp_path, monkeypatch): assert _is_governed_target() is True +def test_disagreeing_managed_versions_are_named_explicitly(monkeypatch): + """A mid-release projection split gets its own actionable diagnostic.""" + import click + + ctx = _workflow_ctx() + monkeypatch.setattr(push_core, "_validate_toml_or_exit", lambda dry_run: None) + monkeypatch.setattr( + "goal.governance.delivery.resolve_delivery_policy", + lambda *a, **k: _pull_request_policy(), + ) + monkeypatch.setattr( + "goal.governance.delivery.validate_delivery_ready", lambda delivery: None + ) + monkeypatch.setattr( + "goal.governance.delivery.resolve_pull_request_ticket", + lambda policy, ticket, **kwargs: None, + ) + monkeypatch.setattr( + push_core, "_managed_version_values", lambda: ["2.2.4", "2.2.5"] + ) + + with pytest.raises(click.ClickException, match="Managed version sources disagree"): + push_core.execute_push_workflow( + ctx, + bump=None, + no_tag=False, + no_changelog=False, + no_version_sync=False, + message=None, + dry_run=False, + yes=True, + markdown=False, + split=False, + ticket=None, + abstraction=None, + todo=False, + ) + + def test_unreleased_sources_without_prebumped_version_name_the_route(monkeypatch): """Unreleased work after the last tag is not a pending publish completion.""" import click @@ -260,6 +302,9 @@ def test_unreleased_sources_without_prebumped_version_name_the_route(monkeypatch "goal.governance.delivery.resolve_pull_request_ticket", lambda policy, ticket, **kwargs: None, ) + monkeypatch.setattr( + push_core, "_managed_version_values", lambda: ["2.2.5"] + ) monkeypatch.setattr( push_core, "_has_committed_unreleased_sources", lambda: True )