From 0fb817a81a8ad987c652718af4a1758eaa6fd44c Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Mon, 2 Nov 2026 16:58:31 +0100 Subject: [PATCH] feat: name where a functionality stands among those of its module The func-id says which functionality is being implemented. It does not say how far the module has come, which the number said before the renderer took func-ids. The TUI and the log now read "add-login-button (3/12)". The count is over every functionality of the module, not over a render range, so the number says where the render is in the specification whatever part of it was asked for. Both lines ask the render context for the position, so they cannot disagree. --- plain2code_events.py | 2 + plain2code_utils.py | 12 +++ .../actions/render_functional_requirement.py | 24 +++-- render_machine/render_context.py | 10 ++- tests/test_functionality_position.py | 90 +++++++++++++++++++ tui/state_handlers.py | 4 +- 6 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 tests/test_functionality_position.py diff --git a/plain2code_events.py b/plain2code_events.py index f9fb40b4..0f5891ec 100644 --- a/plain2code_events.py +++ b/plain2code_events.py @@ -22,6 +22,8 @@ class RenderContextSnapshot: unit_tests_running_context: Optional[UnitTestsRunningContext] script_execution_history: Optional[ScriptExecutionHistory] module_name: str + # Where the functionality stands among those of its module, as "3/12". None before one is picked. + functionality_position: Optional[str] = None @dataclass diff --git a/plain2code_utils.py b/plain2code_utils.py index 6ef6d08f..de2f6593 100644 --- a/plain2code_utils.py +++ b/plain2code_utils.py @@ -31,6 +31,18 @@ def format_duration_hms(total_seconds: float) -> str: } +def functionality_position(frids: list[str], frid: Optional[str]) -> Optional[str]: + """Where a functionality stands among the functionalities of its module, as "3/12". + + The count is over every functionality of the module, not over a render range, so the number + says where the render is in the specification whatever part of it was asked for. + """ + if frid is None or frid not in frids: + return None + + return f"{frids.index(frid) + 1}/{len(frids)}" + + def qualified_frid(module_name: Optional[str], frid: Optional[str]) -> Optional[str]: """A functionality named as `:`. diff --git a/render_machine/actions/render_functional_requirement.py b/render_machine/actions/render_functional_requirement.py index 7bee5d3f..31ab95de 100644 --- a/render_machine/actions/render_functional_requirement.py +++ b/render_machine/actions/render_functional_requirement.py @@ -19,6 +19,21 @@ class RenderFunctionalRequirement(BaseAction): FUNCTIONAL_REQUIREMENT_TOO_COMPLEX_OUTCOME = "functional_requirement_too_complex" ITERATION_LIMIT_EXCEEDED_OUTCOME = "frid_iteration_limit_exceeded" + @staticmethod + def functionality_heading(render_context) -> str: + """The block logged when a functionality starts: its module, its ID, and where it stands.""" + msg = "-------------------------------------\n" + msg += f"Module: {render_context.module_name}\n" + msg += f"Rendering functionality {render_context.frid_context.frid}" + position = render_context.current_functionality_position() + if position: + msg += f" ({position})" + if render_context.frid_context.functional_requirement_render_attempts > 1: + msg += f", attempt number {render_context.frid_context.functional_requirement_render_attempts}/{MAX_CODE_GENERATION_RETRIES}" + msg += f":\n{render_context.frid_context.functional_requirement_text}\n" + msg += "-------------------------------------" + return msg + def execute(self, render_context: RenderContext, _previous_action_payload: Any | None): if render_context.frid_context.functional_requirement_render_attempts >= MAX_CODE_GENERATION_RETRIES: # A render attempt is only restarted once its unit tests used up every fix round. @@ -49,14 +64,7 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | ) _, memory_files_content = MemoryManager.fetch_memory_files(render_context.memory_manager.memory_folder) - msg = "-------------------------------------\n" - msg += f"Module: {render_context.module_name}\n" - msg += f"Rendering functionality {render_context.frid_context.frid}" - if render_context.frid_context.functional_requirement_render_attempts > 1: - msg += f", attempt number {render_context.frid_context.functional_requirement_render_attempts}/{MAX_CODE_GENERATION_RETRIES}" - msg += f":\n{render_context.frid_context.functional_requirement_text}\n" - msg += "-------------------------------------" - console.info(msg) + console.info(self.functionality_heading(render_context)) try: render_utils.print_inputs(render_context, existing_files_content, "Files sent as input to code generation:") diff --git a/render_machine/render_context.py b/render_machine/render_context.py index 07633285..9bca66ae 100644 --- a/render_machine/render_context.py +++ b/render_machine/render_context.py @@ -10,7 +10,7 @@ from plain2code_console import RETRY_COLOR, console from plain2code_events import RenderContextSnapshot from plain2code_state import RunState -from plain2code_utils import qualified_frid +from plain2code_utils import functionality_position, qualified_frid from plain_modules import PlainModule from render_machine import triggers from render_machine.conformance_tests import CONFORMANCE_TESTS_DEFINITION_FILE_NAME, ConformanceTests @@ -122,6 +122,13 @@ def dispatch_error(self, error_message: str): self.last_error_message = error_message self.machine.dispatch(triggers.HANDLE_ERROR) + def current_functionality_position(self) -> Optional[str]: + """Where the functionality being implemented stands among those of its module, as "3/12".""" + return functionality_position( + list(plain_spec.get_frids(self.plain_source_tree)), + self.frid_context.frid if self.frid_context else None, + ) + def create_snapshot(self) -> RenderContextSnapshot: return RenderContextSnapshot( frid_context=deepcopy(self.frid_context) if self.frid_context else None, @@ -133,6 +140,7 @@ def create_snapshot(self) -> RenderContextSnapshot: ), script_execution_history=deepcopy(self.script_execution_history), module_name=self.module_name, + functionality_position=self.current_functionality_position(), ) def get_required_modules_functionalities(self): diff --git a/tests/test_functionality_position.py b/tests/test_functionality_position.py new file mode 100644 index 00000000..463a2b0c --- /dev/null +++ b/tests/test_functionality_position.py @@ -0,0 +1,90 @@ +"""The TUI names the functionality being implemented and where it stands in its module. + +The func-id says which functionality it is. It does not say how far the module has come, which the +number before the renderer took func-ids used to say. +""" + +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +import file_utils # noqa: F401 # imported before plain_modules, which it imports back from +from plain2code_events import RenderContextSnapshot +from plain2code_utils import functionality_position +from render_machine.actions.render_functional_requirement import RenderFunctionalRequirement +from tui.state_handlers import FridReadyHandler + +FRIDS = ["add-login-button", "add-logout-button", "remember-the-session"] + + +class TestFunctionalityPosition: + def test_the_first_functionality(self): + assert functionality_position(FRIDS, "add-login-button") == "1/3" + + def test_the_last_functionality(self): + assert functionality_position(FRIDS, "remember-the-session") == "3/3" + + @pytest.mark.parametrize("frid", [None, "no-such-functionality"], ids=["none", "unknown"]) + def test_nothing_to_place(self, frid): + assert functionality_position(FRIDS, frid) is None + + def test_a_module_with_one_functionality(self): + assert functionality_position(["only-one"], "only-one") == "1/1" + + +def _snapshot(position): + return RenderContextSnapshot( + frid_context=SimpleNamespace(frid="add-login-button", functional_requirement_text="Add a login button."), + conformance_tests_running_context=None, + unit_tests_running_context=None, + script_execution_history=None, + module_name="", + functionality_position=position, + ) + + +def _functionality_line(position): + handler = FridReadyHandler(MagicMock(), unittests_script=None, conformance_tests_script=None) + with ( + patch("tui.state_handlers.get_frid_progress") as get_progress, + patch("tui.state_handlers.update_progress_item_status"), + ): + handler.handle([], _snapshot(position), []) + return get_progress.return_value.update_functionality_text.call_args.args[0] + + +def test_the_tui_line_carries_the_position(): + assert _functionality_line("3/12") == "Functionality add-login-button (3/12): Add a login button." + + +def test_the_tui_line_without_a_position_reads_as_before(): + assert _functionality_line(None) == "Functionality add-login-button: Add a login button." + + +def _log_heading(frids, frid, attempts=1): + """The heading the renderer logs when it starts a functionality.""" + render_context = SimpleNamespace( + module_name="shop", + frid_context=SimpleNamespace( + frid=frid, + functional_requirement_text="Add a login button.", + functional_requirement_render_attempts=attempts, + ), + current_functionality_position=lambda: functionality_position(frids, frid), + ) + return RenderFunctionalRequirement.functionality_heading(render_context) + + +def test_the_log_heading_carries_the_position(): + assert "Rendering functionality add-login-button (1/3):" in _log_heading(FRIDS, "add-login-button") + + +def test_the_log_heading_keeps_the_attempt_after_the_position(): + heading = _log_heading(FRIDS, "add-login-button", attempts=2) + + assert "Rendering functionality add-login-button (1/3), attempt number 2/" in heading + + +def test_the_log_heading_without_a_position_reads_as_before(): + assert "Rendering functionality add-login-button:" in _log_heading([], "add-login-button") diff --git a/tui/state_handlers.py b/tui/state_handlers.py index ee6ec2c9..cb842ab6 100644 --- a/tui/state_handlers.py +++ b/tui/state_handlers.py @@ -62,8 +62,10 @@ def handle(self, _: list[str], snapshot: RenderContextSnapshot, _previous_state_ # Update FRID text functionality = qualified_frid(snapshot.module_name, snapshot.frid_context.frid) + # The position says how far the module has come; the func-id alone does not. + position = f" ({snapshot.functionality_position})" if snapshot.functionality_position else "" rendering_functionality_text = ( - f"{tui_components.FRIDProgress.RENDERING_FUNCTIONALITY_TEXT}{functionality}: " + f"{tui_components.FRIDProgress.RENDERING_FUNCTIONALITY_TEXT}{functionality}{position}: " f"{snapshot.frid_context.functional_requirement_text}" ) get_frid_progress(self.tui).update_functionality_text(rendering_functionality_text)