From 866a0fca97812a4e7d947c37ef9d4f2e71bee5f4 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Tue, 8 Sep 2026 22:20:59 +0200 Subject: [PATCH] refactor(goal): separate workspace repository selection --- project/ticket-119/README.md | 25 ++++++++++++ project/ticket-119/intent.json | 29 ++++++++++++++ src/koru/goal_workspace.py | 69 ++++++++++++++++++---------------- 3 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 project/ticket-119/README.md create mode 100644 project/ticket-119/intent.json diff --git a/project/ticket-119/README.md b/project/ticket-119/README.md new file mode 100644 index 00000000..ce0cf554 --- /dev/null +++ b/project/ticket-119/README.md @@ -0,0 +1,25 @@ +# Ticket 119: Separate Goal workspace repository selection + +- **ID**: ticket-119 +- **Owner**: unresolved:human +- **Status**: IN_PROGRESS +- **Workflow state**: EDIT +- **Created**: 2026-09-08 + +SESSION_EXECUTION_AUTHORIZATION: User requested continued refactoring, GitHub publication, deployment and testing. + +AC-01: Extract bounded repository enumeration and dirty-repository selection +from Goal workspace resolution while preserving fail-closed ambiguity and +unreadable-status errors. + +## Acceptance criteria + +- [ ] AC-01: Goal target resolution remains behaviorally identical. + +Validation: 10 focused tests, Koru-driven regressions, Ruff, managed +governance, Docker Compose and compileall passed. + +## 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-119/intent.json b/project/ticket-119/intent.json new file mode 100644 index 00000000..826883e0 --- /dev/null +++ b/project/ticket-119/intent.json @@ -0,0 +1,29 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-119", + "summary": "Separate Goal workspace repository selection", + "workstream": "application", + "classification": { + "kind": "SERVICE", + "priority": "P2", + "origin": "requested" + }, + "allowedPaths": ["src/koru/goal_workspace.py", "tests/test_goal_workspace.py", "project/ticket-119/**"], + "forbiddenPaths": ["project/ticket-*/user-*.md"], + "stacks": ["python", "docker"], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "9c609d3fb5702ff0e41c848f6d384180cbe8bc9f", + "targetBranch": "main", + "outcome": "Publish a behavior-preserving Goal workspace selection extraction.", + "nonGoals": ["No changes to target authority, fail-closed rules, dependencies or public interfaces."], + "complexity": "S", + "estimatedMinutes": 30, + "budgets": {"maxImplementationFiles": 2, "maxAffectedComponents": 1, "maxPublicInterfaceChanges": 0, "maxRuntimeDependencies": 0}, + "architecture": {"status": "accepted", "decision": "Move child repository discovery and dirty selection into a focused helper while retaining resolution errors and explicit repo handling.", "components": [{"name": "goal-workspace", "paths": ["src/koru/goal_workspace.py", "tests/test_goal_workspace.py"]}], "responsibilityChanges": false, "interfaceChanges": [], "dataChanges": [], "ui": {"impact": "none", "states": [], "evidence": []}, "rollback": "Revert protected merge."}, + "runtimeDependencies": [], + "validation": [{"criterion": "AC-01", "commands": ["python -m pytest -q tests/test_goal_workspace.py", "./project/governance-check.sh", "docker compose config --quiet"], "evidence": "Focused regression, Koru run and independent CI."}] + } +} diff --git a/src/koru/goal_workspace.py b/src/koru/goal_workspace.py index 38053c76..7ee2ef9a 100644 --- a/src/koru/goal_workspace.py +++ b/src/koru/goal_workspace.py @@ -114,6 +114,41 @@ def _explicit_project( return candidate +def _select_workspace_repository( + workspace: Path, repositories: tuple[Path, ...], *, + runner: Callable[..., subprocess.CompletedProcess[str]], +) -> Path: + """Select the sole dirty child or fail closed on ambiguity.""" + dirty: list[Path] = [] + unreadable: list[str] = [] + for candidate in repositories: + state = _is_dirty(candidate, runner=runner) + if state is None: + unreadable.append(candidate.name) + elif state: + dirty.append(candidate) + if unreadable: + raise GoalProjectResolutionError( + "Git status failed for workspace repositories; select only after inspection", + workspace, tuple(unreadable), + ) + if len(dirty) == 1: + return dirty[0] + if not repositories: + raise GoalProjectResolutionError( + "--project is neither a Git repository nor an umbrella with Git children", workspace, + ) + if not dirty: + raise GoalProjectResolutionError( + "umbrella workspace has no dirty repository; use --repo to select one", + workspace, tuple(candidate.name for candidate in repositories), + ) + raise GoalProjectResolutionError( + "umbrella workspace has multiple dirty repositories; use --repo to select one", + workspace, tuple(candidate.name for candidate in dirty), + ) + + def resolve_goal_project( project: Path, repo: str | None = None, @@ -152,36 +187,4 @@ def resolve_goal_project( and not child.is_symlink() and _is_git_root(child, runner=runner) ) - dirty: list[Path] = [] - unreadable: list[str] = [] - for candidate in repositories: - state = _is_dirty(candidate, runner=runner) - if state is None: - unreadable.append(candidate.name) - elif state: - dirty.append(candidate) - - if unreadable: - raise GoalProjectResolutionError( - "Git status failed for workspace repositories; select only after inspection", - workspace, - tuple(unreadable), - ) - if len(dirty) == 1: - return dirty[0] - if not repositories: - raise GoalProjectResolutionError( - "--project is neither a Git repository nor an umbrella with Git children", - workspace, - ) - if not dirty: - raise GoalProjectResolutionError( - "umbrella workspace has no dirty repository; use --repo to select one", - workspace, - tuple(candidate.name for candidate in repositories), - ) - raise GoalProjectResolutionError( - "umbrella workspace has multiple dirty repositories; use --repo to select one", - workspace, - tuple(candidate.name for candidate in dirty), - ) + return _select_workspace_repository(workspace, repositories, runner=runner)