From ca585ca4e8e21797e1cd5179f6ed0b93961fc261 Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:03:24 +0800 Subject: [PATCH] refactor(todos): retire legacy Python bridge adapters Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> --- loopx/cli_commands/todo_list.py | 15 -- .../control_plane/effect_runtime_handlers.ts | 2 - loopx/control_plane/todos/list_projection.py | 77 ------ .../todos/next_action_runtime.py | 221 ------------------ loopx/todos.py | 6 - .../test_next_action_writeback.py | 4 - .../test_todo_next_action_settlement.py | 91 -------- tests/control_plane/test_todo_work_counts.py | 15 +- 8 files changed, 4 insertions(+), 427 deletions(-) delete mode 100644 loopx/control_plane/todos/list_projection.py delete mode 100644 loopx/control_plane/todos/next_action_runtime.py diff --git a/loopx/cli_commands/todo_list.py b/loopx/cli_commands/todo_list.py index 3f4a26b16d..c24e915a75 100644 --- a/loopx/cli_commands/todo_list.py +++ b/loopx/cli_commands/todo_list.py @@ -5,7 +5,6 @@ from collections.abc import Callable from pathlib import Path -from ..control_plane.effect_runtime import effect_runtime_result from ..control_plane.todos.list_readback import list_goal_todos from ..control_plane.todos.markdown import render_todo_markdown from .todo_argument_validation import ( @@ -56,19 +55,5 @@ def handle_todo_list_command( append_cli_rollout_event=append_cli_rollout_event, ) output_format = format_name or str(getattr(args, "format", None) or "markdown") - if (payload.get("ok") and not args.todo_id and not args.todo_thin - and "todo_list_record_references" not in payload - and output_format == "json"): - try: - payload = effect_runtime_result( - "todo.context.page", {"list_payload": payload}, - large_local_snapshot=True, - ) - except Exception as exc: - from ..usage_ping import capture_failure - capture_failure(exc) - payload = todo_error_payload( - args, exc, registry_path=registry_path, runtime_root_arg=runtime_root_arg, - ) print_payload(payload, output_format, render_todo_markdown) return 0 if payload.get("ok") else 1 diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index 410a5d72df..a8e037ef8f 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -321,8 +321,6 @@ export function createEffectRuntimeHandlers( ["todo.archive.select", lazyHandler(() => import("./coordination/todo_archive_selection.ts"), ({evaluateCoordinationTodoArchiveSelection}) => evaluateCoordinationTodoArchiveSelection)], ["todo.successor.derive", lazyHandler(() => import("./coordination/todo_successor_derivation.ts"), ({evaluateCoordinationTodoSuccessorDerivation}) => evaluateCoordinationTodoSuccessorDerivation)], ["todo.completion.reduce", lazyHandler(() => import("./todos/completion_transaction.ts"), ({reduceTodoCompletionTransaction}) => reduceTodoCompletionTransaction)], - ["todo.next_action.transition", lazyHandler(() => import("./todos/next_action.ts"), ({transitionTodoNextAction}) => transitionTodoNextAction)], - ["todo.next_action.binding", lazyHandler(() => import("./todos/next_action.ts"), ({projectNextActionBinding}) => projectNextActionBinding)], ["todo.resume_condition.normalize", lazyHandler(() => import("./todos/resume_condition.ts"), ({normalizeTodoResumeWhen}) => normalizeTodoResumeWhen)], ["todo.resume_condition.evaluate", lazyHandler(() => import("./todos/resume_condition.ts"), ({evaluateTodoResumeConditionsFromSource}) => evaluateTodoResumeConditionsFromSource)], ["goal.fallback_disposition.project", lazyHandler(() => import("./goals/goal_frontier/fallback_disposition.ts"), ({projectFallbackDisposition}) => projectFallbackDisposition)], diff --git a/loopx/control_plane/todos/list_projection.py b/loopx/control_plane/todos/list_projection.py deleted file mode 100644 index fa6c9fe40b..0000000000 --- a/loopx/control_plane/todos/list_projection.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Compatibility exports for complete native Todo presentation lenses.""" -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any, Literal -from ..effect_runtime import effect_runtime_result - -_READ_CODEC = json.loads(Path(__file__).with_name("read_legacy_codec.json").read_text(encoding="utf-8")) -_READ_LIST_LAYOUT = _READ_CODEC["list_constants"] -AGENT_LANE_TODO_LIST_PROJECTION_SCHEMA_VERSION = _READ_LIST_LAYOUT["AGENT_LANE_TODO_LIST_PROJECTION_SCHEMA_VERSION"] -AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION = _READ_LIST_LAYOUT["AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION"] -EXPLICIT_LIMIT_TODO_LIST_SUMMARY_SCHEMA_VERSION = _READ_LIST_LAYOUT["EXPLICIT_LIMIT_TODO_LIST_SUMMARY_SCHEMA_VERSION"] -THIN_TODO_LIST_PROJECTION_SCHEMA_VERSION = _READ_LIST_LAYOUT["THIN_TODO_LIST_PROJECTION_SCHEMA_VERSION"] -THIN_TODO_LIST_SUMMARY_SCHEMA_VERSION = _READ_LIST_LAYOUT["THIN_TODO_LIST_SUMMARY_SCHEMA_VERSION"] -AGENT_LANE_TODO_LIST_ITEM_LIMIT = _READ_LIST_LAYOUT["AGENT_LANE_TODO_LIST_ITEM_LIMIT"] -AGENT_LANE_TODO_LIST_TEXT_LIMIT = _READ_LIST_LAYOUT["AGENT_LANE_TODO_LIST_TEXT_LIMIT"] -THIN_TODO_LIST_ITEM_LIMIT_PER_ROLE = _READ_LIST_LAYOUT["THIN_TODO_LIST_ITEM_LIMIT_PER_ROLE"] -THIN_TODO_LIST_NESTED_ITEM_LIMIT = _READ_LIST_LAYOUT["THIN_TODO_LIST_NESTED_ITEM_LIMIT"] -THIN_TODO_LIST_NESTED_DICT_FIELD_LIMIT = _READ_LIST_LAYOUT["THIN_TODO_LIST_NESTED_DICT_FIELD_LIMIT"] -THIN_TODO_LIST_ITEM_FIELDS = tuple(_READ_LIST_LAYOUT["THIN_TODO_LIST_ITEM_FIELDS"]) -AGENT_LANE_HOT_PATH_VIEW = "agent_lane_hot_path" -EXPLICIT_LIMIT_COLD_PATH_VIEW = "explicit_limit_cold_path" -THIN_EXPLICIT_VIEW = "thin_explicit_view" -TodoListProjectionView = Literal["agent_lane_hot_path", "explicit_limit_cold_path"] - - -def compact_thin_todo_list_payload(payload): - return effect_runtime_result("todo.context.page", {"thin_payload": payload}, large_local_snapshot=True) - - -def compact_agent_lane_todo_summary(summary, *, role): - return effect_runtime_result("todo.context.page", {"summary_payload": summary, "role": role, "limit": None}, large_local_snapshot=True) - - -def compact_explicit_limit_todo_summary(summary, *, role, item_limit): - return effect_runtime_result("todo.context.page", {"summary_payload": summary, "role": role, "limit": item_limit}, large_local_snapshot=True) - - -def compact_thin_todo_summary(summary, *, role, items_matched, items_returned, item_limit_per_role): - return effect_runtime_result("todo.context.page", {"thin_summary_payload": summary, "details": { - "role": role, "items_matched": items_matched, "items_returned": items_returned, - "item_limit_per_role": item_limit_per_role}}, large_local_snapshot=True) - - -def todo_item_relations(item: dict[str, Any]) -> dict[str, Any]: - """Project the stable relationship fields for one todo list item.""" - - relations: dict[str, Any] = {} - for key in _READ_CODEC["relation_fields"]: - value = item.get(key) - if value is not None and value != []: - relations[key] = value - return relations - - -def todo_list_projection_contract( - *, - matched_todo_count: int, - returned_todo_count: int, - view: TodoListProjectionView = AGENT_LANE_HOT_PATH_VIEW, - item_limit_per_role: int = AGENT_LANE_TODO_LIST_ITEM_LIMIT, - full_detail_cold_paths: tuple[str, ...] = ( - "todo list with --role, --status, or --todo-id", - "todo list without --agent-id", - "active state", - ), -) -> dict[str, Any]: - return { - "schema_version": AGENT_LANE_TODO_LIST_PROJECTION_SCHEMA_VERSION, - "view": view, - "matched_todo_count": matched_todo_count, - "returned_todo_count": returned_todo_count, - "item_limit_per_role": item_limit_per_role, - "counts_cover_full_match": True, - "full_detail_cold_paths": list(full_detail_cold_paths), - } diff --git a/loopx/control_plane/todos/next_action_runtime.py b/loopx/control_plane/todos/next_action_runtime.py deleted file mode 100644 index e1d9fa0110..0000000000 --- a/loopx/control_plane/todos/next_action_runtime.py +++ /dev/null @@ -1,221 +0,0 @@ -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any - -from ..effect_runtime import effect_runtime_result -from .active_state_editing import section_bounds, todo_blocks -from .contract import ( - TODO_STATUS_OPEN, - normalize_todo_id, - normalize_todo_id_list, - normalize_todo_status, -) -from .todo_semantics import todo_priority_label - - -TODO_NEXT_ACTION_REQUEST_SCHEMA = "loopx_todo_next_action_transition_v1" -TODO_NEXT_ACTION_RESULT_SCHEMA = "loopx_todo_next_action_result_v0" -NEXT_ACTION_HEADING = "## Next Action" - - -def _next_action_projection_bounds(lines: list[str]) -> tuple[int, int] | None: - start = next( - ( - index - for index, line in enumerate(lines) - if line.strip() == NEXT_ACTION_HEADING - ), - None, - ) - if start is None: - return None - end = len(lines) - for index in range(start + 1, len(lines)): - if lines[index].startswith("## "): - end = index - break - return start, end - - -def _agent_todo_snapshots(lines: list[str]) -> list[dict[str, Any]]: - bounds = section_bounds(lines, "agent") - if bounds is None: - return [] - start, end, section = bounds - snapshots: list[dict[str, Any]] = [] - for block in todo_blocks( - lines, - start, - end, - role="agent", - source_section=section, - ): - todo_id = normalize_todo_id(block.get("todo_id")) - if not todo_id: - raise RuntimeError("Agent Todo projection produced an invalid todo_id") - snapshots.append( - { - "todo_id": todo_id, - "status": normalize_todo_status(block.get("status")) - or TODO_STATUS_OPEN, - "task_class": str(block.get("task_class") or "").strip() or None, - "priority": todo_priority_label(block, text_mode="prefix"), - "claimed_by": str(block.get("claimed_by") or "").strip() or None, - "text": str(block.get("text") or "").strip(), - "index": int(block.get("index") or 0), - "completion_continuation": ( - str(block.get("completion_continuation") or "").strip() or None - ), - "successor_todo_ids": normalize_todo_id_list( - block.get("successor_todo_ids") - ), - } - ) - return snapshots - - -def _materialized_todo_ids(lines: list[str]) -> list[str]: - todo_ids: list[str] = [] - for role in ("user", "agent"): - bounds = section_bounds(lines, role) - if bounds is None: - continue - start, end, section = bounds - for block in todo_blocks( - lines, - start, - end, - role=role, - source_section=section, - ): - todo_id = normalize_todo_id(block.get("todo_id")) - if todo_id and todo_id not in todo_ids: - todo_ids.append(todo_id) - return todo_ids - - -def _apply_transition( - lines: list[str], - *, - operation: str, - params: Mapping[str, Any], -) -> dict[str, Any]: - projection_bounds = _next_action_projection_bounds(lines) - if projection_bounds is None: - projection_lines: list[str] = [] - else: - start, end = projection_bounds - projection_lines = lines[start:end] - result = effect_runtime_result( - "todo.next_action.transition", - { - "schema_version": TODO_NEXT_ACTION_REQUEST_SCHEMA, - "operation": operation, - "lines": projection_lines, - **dict(params), - }, - ) - if not isinstance(result, Mapping): - raise RuntimeError("TypeScript Todo Next Action result must be an object") - if ( - result.get("schema_version") != TODO_NEXT_ACTION_RESULT_SCHEMA - or result.get("operation") != operation - or not isinstance(result.get("changed"), bool) - or not isinstance(result.get("matched"), bool) - or not isinstance(result.get("outcome"), str) - ): - raise RuntimeError("TypeScript Todo Next Action result shape mismatch") - updated_lines = result.get("lines") - if not isinstance(updated_lines, list) or not all( - isinstance(line, str) for line in updated_lines - ): - raise RuntimeError("TypeScript Todo Next Action lines shape mismatch") - changed = updated_lines != projection_lines - if changed is not result["changed"]: - raise RuntimeError("TypeScript Todo Next Action changed flag mismatch") - if changed: - if projection_bounds is None: - raise RuntimeError( - "TypeScript Todo Next Action changed a missing projection" - ) - start, end = projection_bounds - lines[start:end] = updated_lines - materialized_result = dict(result) - materialized_result["lines"] = list(lines) - return materialized_result - - -def reconcile_added_todo_next_action( - lines: list[str], - *, - added_todo_id: str, -) -> bool: - """Let higher-priority task work replace a lower-priority typed route.""" - - normalized_todo_id = normalize_todo_id(added_todo_id) - if not normalized_todo_id: - raise ValueError("Next Action reconciliation requires a valid added todo_id") - result = _apply_transition( - lines, - operation="reconcile_added", - params={ - "todo_id": normalized_todo_id, - "agent_todos": _agent_todo_snapshots(lines), - }, - ) - return bool(result["changed"]) - - -def apply_added_todo_next_action( - lines: list[str], - *, - role: str, - add_result: dict[str, Any], -) -> bool: - """Return whether todo addition or its typed Next Action projection changed.""" - - changed = bool(add_result["changed"]) - todo_id = str(add_result.get("todo_id") or "") - if ( - not changed - or role != "agent" - or add_result.get("task_class") != "advancement_task" - or not todo_id - ): - return changed - return reconcile_added_todo_next_action(lines, added_todo_id=todo_id) or changed - - -def settle_completed_todo_next_action( - lines: list[str], - *, - completed_todo_id: str, -) -> bool: - """Reconcile the generated Next Action from the final Todo transaction state.""" - - normalized_todo_id = normalize_todo_id(completed_todo_id) - if not normalized_todo_id: - raise ValueError("Next Action settlement requires a valid completed todo_id") - result = _apply_transition( - lines, - operation="settle_completion", - params={ - "todo_id": normalized_todo_id, - "agent_todos": _agent_todo_snapshots(lines), - "materialized_todo_ids": _materialized_todo_ids(lines), - }, - ) - return bool(result["changed"]) - - -def bound_next_action_todo_ids(state_text: str) -> set[str]: - """Decode the existing typed binding, without inferring IDs from prose.""" - if "loopx:next-action" not in state_text: - return set() - lines = state_text.splitlines() - bounds = _next_action_projection_bounds(lines) - if bounds is None: - return set() - result = effect_runtime_result("todo.next_action.binding", {"lines": lines[bounds[0]:bounds[1]]}) - return {result["todo_id"]} if result.get("todo_id") else set() diff --git a/loopx/todos.py b/loopx/todos.py index 50f9335af3..2f74bb7a5e 100644 --- a/loopx/todos.py +++ b/loopx/todos.py @@ -86,12 +86,6 @@ "user_todo_completion_metadata_updates": "loopx.control_plane.todos.completion_transaction", "completion_validation_module": "loopx.control_plane.todos", "serialize_added_todo_payload": "loopx.control_plane.todos.mutation_response", - "apply_added_todo_next_action": "loopx.control_plane.todos.next_action_runtime", - "settle_completed_todo_next_action": "loopx.control_plane.todos.next_action_runtime", - "compact_agent_lane_todo_summary": "loopx.control_plane.todos.list_projection", - "compact_thin_todo_list_payload": "loopx.control_plane.todos.list_projection", - "todo_item_relations": "loopx.control_plane.todos.list_projection", - "todo_list_projection_contract": "loopx.control_plane.todos.list_projection", "exact_archived_todo_summaries": "loopx.control_plane.todos.goal_todo_projection", "goal_todo_summaries": "loopx.control_plane.todos.goal_todo_projection", "todo_summaries_from_fields": "loopx.control_plane.todos.goal_todo_projection", diff --git a/tests/control_plane/test_next_action_writeback.py b/tests/control_plane/test_next_action_writeback.py index bf189c3805..7295acb5ad 100644 --- a/tests/control_plane/test_next_action_writeback.py +++ b/tests/control_plane/test_next_action_writeback.py @@ -14,7 +14,6 @@ from loopx.control_plane.work_items.recommendation_source_io import RecommendationWritebackRejected from loopx.status import collect_status from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted -from loopx.control_plane.todos.next_action_runtime import settle_completed_todo_next_action from tests.control_plane import test_todo_projection_concurrency as projection_fixtures canonical_projection = projection_fixtures.canonical_projection @@ -216,9 +215,6 @@ def test_step_does_not_destroy_binding_or_block_completion_projection(tmp_path): state.write_text(bound) write(registry, runtime) assert state.read_text() == bound - completed = bound.replace("todo_parser status=open", "todo_parser status=done").splitlines() - assert settle_completed_todo_next_action(completed, completed_todo_id="todo_parser") - assert "todo_id=todo_evaluate" in "\n".join(completed) run_json_cli("todo", "complete", "--goal-id", "next-action-goal", "--todo-id", "todo_parser", "--agent-id", "agent-a", "--evidence", "fixture completion", registry_path=registry, runtime_root=runtime) diff --git a/tests/control_plane/test_todo_next_action_settlement.py b/tests/control_plane/test_todo_next_action_settlement.py index f5f27a3cad..4c5a97c349 100644 --- a/tests/control_plane/test_todo_next_action_settlement.py +++ b/tests/control_plane/test_todo_next_action_settlement.py @@ -8,15 +8,6 @@ from loopx.control_plane.coordination.runtime_shadow import ( build_todo_runtime_shadow_projection, ) -from loopx.control_plane.todos.next_action_runtime import ( - _agent_todo_snapshots, - apply_added_todo_next_action, - reconcile_added_todo_next_action, -) -from loopx.control_plane.todos.projection import ( - TODO_MISSING_PRIORITY_RANK, - todo_priority_rank, -) from loopx.state_projection import active_state_next_action_entries from loopx.state_refresh import build_state_refresh_record from loopx.todos import add_goal_todo, complete_goal_todo @@ -38,28 +29,6 @@ def _initialize_canonical(tmp_path: Path, state: Path) -> None: ) -def test_agent_todo_snapshot_uses_structured_prefix_priority() -> None: - snapshots = _agent_todo_snapshots( - [ - "## Agent Todo", - "", - "- [ ] [P2-review] Validate the typed priority contract.", - " ", - "- [ ] Explain why a P0 mention in prose is not priority metadata.", - " ", - "", - "## Next Action", - "", - ] - ) - - assert [snapshot["priority"] for snapshot in snapshots] == ["P2", None] - assert [todo_priority_rank(snapshot["priority"]) for snapshot in snapshots] == [ - 2, - TODO_MISSING_PRIORITY_RANK, - ] - - def _write_fixture(tmp_path: Path, *, next_action: str) -> tuple[Path, Path]: repo = tmp_path / "repo" repo.mkdir() @@ -297,66 +266,6 @@ def test_higher_priority_todo_does_not_take_over_another_claimed_lane( ] -def test_higher_priority_todo_preserves_multi_entry_operator_route() -> None: - lines = [ - "## Agent Todo", - "", - "- [ ] [P1] Run the generated startup check.", - " ", - "- [ ] [P0] Implement the requested behavior.", - " ", - "", - "## Next Action", - "", - "- [P1] Run the generated startup check.", - "", - "- Preserve this operator-authored route.", - "", - ] - - assert reconcile_added_todo_next_action(lines, added_todo_id="todo_task") is False - assert "- Preserve this operator-authored route." in lines - assert "todo_id=todo_startup" in "\n".join(lines) - - -def test_unchanged_and_non_advancement_adds_skip_the_ts_transition( - monkeypatch, -) -> None: - def reject_unexpected_transition(*_args, **_kwargs): - raise AssertionError("non-candidate Todo addition crossed the TS boundary") - - monkeypatch.setattr( - "loopx.control_plane.todos.next_action_runtime.effect_runtime_result", - reject_unexpected_transition, - ) - lines = ["## Next Action", "", "- Preserve the existing route.", ""] - - assert ( - apply_added_todo_next_action( - lines, - role="agent", - add_result={ - "changed": False, - "todo_id": "todo_existing", - "task_class": "advancement_task", - }, - ) - is False - ) - assert ( - apply_added_todo_next_action( - lines, - role="agent", - add_result={ - "changed": True, - "todo_id": "todo_monitor", - "task_class": "continuous_monitor", - }, - ) - is True - ) - - def test_refresh_record_preserves_a_long_wrapped_next_action_losslessly( tmp_path: Path, ) -> None: diff --git a/tests/control_plane/test_todo_work_counts.py b/tests/control_plane/test_todo_work_counts.py index 8f9bb3714f..0db36a0a22 100644 --- a/tests/control_plane/test_todo_work_counts.py +++ b/tests/control_plane/test_todo_work_counts.py @@ -36,20 +36,13 @@ def test_incomplete_legacy_summary_cannot_invent_hidden_advancement(): assert counts["complete"] is False -def test_list_and_status_compactors_preserve_semantic_counts(): - from loopx.control_plane.todos.list_projection import ( - compact_agent_lane_todo_summary, compact_explicit_limit_todo_summary, compact_thin_todo_summary, - ) +def test_status_compactor_preserves_semantic_counts(): from loopx.control_plane.todos.quota_summary import _compact_agent_lane_status_todo_summary summary = compact_todo_group([work(i) for i in range(21)], source_section="Agent Todo", role="agent") - projections = [compact_agent_lane_todo_summary(summary, role="agent"), - compact_explicit_limit_todo_summary(summary, role="agent", item_limit=1), - compact_thin_todo_summary(summary, role="agent", items_matched=21, items_returned=1, item_limit_per_role=1), - _compact_agent_lane_status_todo_summary(summary, role="agent")] - for projected in projections: - assert todo_summary_open_task_counts(projected)["advancement"] == 21 - assert projected["work_counts"]["complete"] is True + projected = _compact_agent_lane_status_todo_summary(summary, role="agent") + assert todo_summary_open_task_counts(projected)["advancement"] == 21 + assert projected["work_counts"]["complete"] is True def test_incomplete_scoped_snapshot_does_not_certify_monitor_only():