From 0f1bcb9a5d87200d1ab3ae30eee7659e770411b5 Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Tue, 9 Jun 2026 11:28:46 +0200 Subject: [PATCH 01/11] move frid iteration check to action --- .../actions/render_functional_requirement.py | 14 ++++++++++++++ render_machine/render_context.py | 15 --------------- render_machine/state_machine_config.py | 6 ++---- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/render_machine/actions/render_functional_requirement.py b/render_machine/actions/render_functional_requirement.py index cea67a5..35511bd 100644 --- a/render_machine/actions/render_functional_requirement.py +++ b/render_machine/actions/render_functional_requirement.py @@ -16,8 +16,22 @@ class RenderFunctionalRequirement(BaseAction): SUCCESSFUL_OUTCOME = "code_and_unit_tests_generated" FUNCTIONAL_REQUIREMENT_TOO_COMPLEX_OUTCOME = "functional_requirement_too_complex" + ITERATION_LIMIT_EXCEEDED_OUTCOME = "frid_iteration_limit_exceeded" def execute(self, render_context: RenderContext, _previous_action_payload: Any | None): + if render_context.frid_context.functional_requirement_render_attempts >= MAX_CODE_GENERATION_RETRIES: + error_msg = f"Unittests could not be fixed after rendering the functionality {render_context.frid_context.frid} for the {MAX_CODE_GENERATION_RETRIES} times." + render_context.last_error_message = error_msg + return self.ITERATION_LIMIT_EXCEEDED_OUTCOME, None + + render_context.frid_context.functional_requirement_render_attempts += 1 + + if render_context.frid_context.functional_requirement_render_attempts > 1: + console.info( + f"Unittests could not be fixed after rendering the functionality. " + f"Restarting rendering functionality {render_context.frid_context.frid} from scratch." + ) + render_utils.revert_changes_for_frid(render_context) existing_files, existing_files_content = ImplementationCodeHelpers.fetch_existing_files( render_context.build_folder diff --git a/render_machine/render_context.py b/render_machine/render_context.py index ecda27f..ab8512e 100644 --- a/render_machine/render_context.py +++ b/render_machine/render_context.py @@ -23,7 +23,6 @@ ) MAX_UNITTEST_FIX_ATTEMPTS = 20 -MAX_CODE_GENERATION_RETRIES = 2 MAX_CONFORMANCE_TEST_RERENDER_ATTEMPTS = 1 MAX_REFACTORING_ITERATIONS = 5 MAX_CONFORMANCE_TEST_FIX_ATTEMPTS = 20 @@ -164,20 +163,6 @@ def start_implementing_frid(self): ) return - def check_frid_iteration_limit(self): - if self.frid_context.functional_requirement_render_attempts >= MAX_CODE_GENERATION_RETRIES: - error_msg = f"Unittests could not be fixed after rendering the functionality {self.frid_context.frid} for the {MAX_CODE_GENERATION_RETRIES} times." - self.dispatch_error(error_msg) - - self.frid_context.functional_requirement_render_attempts += 1 - - if self.frid_context.functional_requirement_render_attempts > 1: - # this if is intended just for logging - console.info( - f"Unittests could not be fixed after rendering the functionality. " - f"Restarting rendering the functionality {self.frid_context.frid} from scratch." - ) - def has_next_frid(self) -> bool: next_frid = plain_spec.get_next_frid(self.plain_source_tree, self.frid_context.frid) if self.render_range is None or len(self.render_range) == 0: diff --git a/render_machine/state_machine_config.py b/render_machine/state_machine_config.py index 639e1eb..147a539 100644 --- a/render_machine/state_machine_config.py +++ b/render_machine/state_machine_config.py @@ -73,6 +73,7 @@ def get_action_result_triggers_map(self) -> Dict[str, str]: PrepareRepositories.SUCCESSFUL_OUTCOME: triggers.START_RENDER, RenderFunctionalRequirement.SUCCESSFUL_OUTCOME: triggers.RENDER_FUNCTIONAL_REQUIREMENT, RenderFunctionalRequirement.FUNCTIONAL_REQUIREMENT_TOO_COMPLEX_OUTCOME: triggers.HANDLE_ERROR, + RenderFunctionalRequirement.ITERATION_LIMIT_EXCEEDED_OUTCOME: triggers.HANDLE_ERROR, RunUnitTests.SUCCESSFUL_OUTCOME: triggers.MARK_UNIT_TESTS_PASSED, RunUnitTests.FAILED_OUTCOME: triggers.MARK_UNIT_TESTS_FAILED, RunUnitTests.UNRECOVERABLE_ERROR_OUTCOME: triggers.HANDLE_ERROR, @@ -183,10 +184,7 @@ def get_states(self, render_context: RenderContext) -> List[Any]: "on_exit": render_context.finish_implementing_frid, "children": [ {"name": States.STEP_COMPLETED.value}, - { - "name": States.READY_FOR_FRID_IMPLEMENTATION.value, - "on_enter": render_context.check_frid_iteration_limit, - }, + States.READY_FOR_FRID_IMPLEMENTATION.value, self.get_processing_unit_tests_states( render_context, render_context._on_unit_test_limit_exceeded_in_implementation ), From c62dec3b304e5398cf5c30d0fcc501d85cf3c72b Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Thu, 4 Jun 2026 10:00:52 +0200 Subject: [PATCH 02/11] add --rerender flag for re-rendering a single already-rendered FRID - New `--rerender N` CLI flag renders a single FRID on top of current HEAD without reverting the build repo, unlike `--render-range`/`--render-from` - PrepareRepositories validates the target FRID and loads the old spec text from module_metadata.json so the LLM can produce a targeted patch - Stale conformance tests for the FRID are deleted (uncommitted) before rendering so CommitConformanceTestsChanges picks them up naturally - FinishFunctionalRequirement and CommitConformanceTestsChanges select FUNCTIONAL_REQUIREMENT_REIMPLEMENTED_COMMIT_MESSAGE at runtime via render_context.is_rerender, keeping --render-range revert logic intact - get_last_rendered_functionality searches both "implemented" and "reimplemented" patterns and returns the more recent match --- codeplain_REST_api.py | 8 ++++ git_utils.py | 23 +++++++-- module_renderer.py | 3 ++ plain2code.py | 14 ++++-- plain2code_arguments.py | 7 +++ .../commit_conformance_tests_changes.py | 10 ++-- .../actions/finish_functional_requirement.py | 20 ++++++-- .../actions/prepare_repositories.py | 47 +++++++++++++++++++ .../actions/render_conformance_tests.py | 1 + .../actions/render_functional_requirement.py | 2 + render_machine/render_context.py | 3 ++ render_machine/state_machine_config.py | 5 +- 12 files changed, 124 insertions(+), 19 deletions(-) diff --git a/codeplain_REST_api.py b/codeplain_REST_api.py index 33542d2..30b1409 100644 --- a/codeplain_REST_api.py +++ b/codeplain_REST_api.py @@ -180,6 +180,8 @@ def render_functional_requirement( required_modules: dict, include_unittests: bool, run_state: RunState, + is_reimplementation: bool = False, + old_functional_requirement_text: Optional[str] = None, ) -> dict[str, str]: """ Renders the content of a functionality based on the provided ID, @@ -219,8 +221,12 @@ def render_functional_requirement( "module_name": module_name, "required_modules": required_modules, "include_unittests": include_unittests, + "is_reimplementation": is_reimplementation, } + if old_functional_requirement_text is not None: + payload["old_functional_requirement_text"] = old_functional_requirement_text + return self.post_request(endpoint_url, headers, payload, run_state) def fix_unittests_issue( @@ -317,6 +323,7 @@ def render_conformance_tests( conformance_tests_json, all_acceptance_tests, run_state: RunState, + is_reimplementation: bool = False, ): endpoint_url = f"{self.api_url}/render_conformance_tests" headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"} @@ -333,6 +340,7 @@ def render_conformance_tests( "conformance_tests_folder_name": conformance_tests_folder_name, "conformance_tests_json": conformance_tests_json, "all_acceptance_tests": all_acceptance_tests, + "is_reimplementation": is_reimplementation, } response = self.post_request(endpoint_url, headers, payload, run_state) diff --git a/git_utils.py b/git_utils.py index ae064d4..a1c165b 100644 --- a/git_utils.py +++ b/git_utils.py @@ -17,6 +17,7 @@ # Following messages are used as checkpoints in the git history # Changing them will break backwards compatibility so change them with care FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE = "[Codeplain] functionality ID (FRID):{} fully implemented" +FUNCTIONAL_REQUIREMENT_REIMPLEMENTED_COMMIT_MESSAGE = "[Codeplain] functionality ID (FRID):{} fully reimplemented" INITIAL_COMMIT_MESSAGE = "[Codeplain] Initial module commit" BASE_FOLDER_COMMIT_MESSAGE = "[Codeplain] Initialize build with Base Folder content" @@ -440,9 +441,23 @@ def get_last_rendered_functionality(repo_path: Union[str, os.PathLike]) -> tuple return None, None repo = Repo(repo_path) - grep_pattern = FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format(".*") - grep_pattern = grep_pattern.replace("[", "\\[").replace("]", "\\]") - commit_sha = repo.git.rev_list(repo.active_branch.name, "--grep", grep_pattern, "-n", "1") + + implemented_pattern = FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format(".*") + implemented_pattern = implemented_pattern.replace("[", "\\[").replace("]", "\\]") + implemented_sha = repo.git.rev_list(repo.active_branch.name, "--grep", implemented_pattern, "-n", "1") + + reimplemented_pattern = FUNCTIONAL_REQUIREMENT_REIMPLEMENTED_COMMIT_MESSAGE.format(".*") + reimplemented_pattern = reimplemented_pattern.replace("[", "\\[").replace("]", "\\]") + reimplemented_sha = repo.git.rev_list(repo.active_branch.name, "--grep", reimplemented_pattern, "-n", "1") + + # Pick the more recent of the two (rev-list outputs most-recent first) + if implemented_sha and reimplemented_sha: + all_shas = repo.git.rev_list(repo.active_branch.name).splitlines() + implemented_idx = all_shas.index(implemented_sha) + reimplemented_idx = all_shas.index(reimplemented_sha) + commit_sha = implemented_sha if implemented_idx < reimplemented_idx else reimplemented_sha + else: + commit_sha = implemented_sha or reimplemented_sha if not commit_sha: # Repo was interrupted during the first functionality, fallback to initial commit and provide only module name @@ -469,7 +484,7 @@ def get_last_rendered_functionality(repo_path: Union[str, os.PathLike]) -> tuple if isinstance(commit_message, bytes): commit_message = commit_message.decode("utf-8") - match = re.search(r"FRID\):(\S+) fully implemented", commit_message) + match = re.search(r"FRID\):(\S+) fully (?:re)?implemented", commit_message) if not match: raise InvalidGitRepositoryError( "Git repository is in an invalid state. Could not find frid in finished commit." diff --git a/module_renderer.py b/module_renderer.py index 64808d6..20ca7c9 100644 --- a/module_renderer.py +++ b/module_renderer.py @@ -27,6 +27,7 @@ def __init__( event_bus: EventBus, stop_event: threading.Event | None = None, enter_pause_event: threading.Event | None = None, + is_rerender: bool = False, ): self.codeplainAPI = codeplainAPI self.plain_module = plain_module @@ -37,6 +38,7 @@ def __init__( self.event_bus = event_bus self.stop_event = stop_event self.enter_pause_event = enter_pause_event + self.is_rerender = is_rerender def _build_render_context_for_module( self, @@ -65,6 +67,7 @@ def _build_render_context_for_module( test_script_timeout=self.args.test_script_timeout, stop_event=self.stop_event, enter_pause_event=self.enter_pause_event, + is_rerender=self.is_rerender, ) def _render_module( diff --git a/plain2code.py b/plain2code.py index 2a822c9..56fd5bd 100644 --- a/plain2code.py +++ b/plain2code.py @@ -139,12 +139,17 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI): def render(args, run_state: RunState, event_bus: EventBus, default_log_level: str = "INFO"): # noqa: C901 template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS) - # Compute render range from either --render-range or --render-from + # Compute render range from either --render-range, --render-from, or --rerender render_range = None - if args.render_range or args.render_from: + is_rerender = False + if args.render_range or args.render_from or args.rerender: # Parse the plain file to get the plain_source for FRID extraction _, plain_source, _ = plain_file.plain_file_parser(args.filename, template_dirs) - render_range = plain_spec.compute_render_range(args, plain_source) + if args.rerender: + render_range = plain_spec.get_render_range(args.rerender + "," + args.rerender, plain_source) + is_rerender = True + else: + render_range = plain_spec.compute_render_range(args, plain_source) codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console) assert args.api is not None and args.api != "", "API URL is required" @@ -164,7 +169,7 @@ def render(args, run_state: RunState, event_bus: EventBus, default_log_level: st ) render_choice = None - if render_range is None: + if render_range is None and not is_rerender: plain_module_render_state = get_plain_module_render_state(plain_module) if plain_module_render_state is not None: render_choices = get_render_choices(plain_module, plain_module_render_state, args.force_render) @@ -212,6 +217,7 @@ def render(args, run_state: RunState, event_bus: EventBus, default_log_level: st event_bus, stop_event=stop_event, enter_pause_event=enter_pause_event, + is_rerender=is_rerender, ) render_error: list[Exception] = [] diff --git a/plain2code_arguments.py b/plain2code_arguments.py index 67be118..1ca02d1 100644 --- a/plain2code_arguments.py +++ b/plain2code_arguments.py @@ -226,6 +226,13 @@ def create_parser(): help="Continue generation starting from this specific functionality (e.g. `2`). " "The functionality with this ID will be included in the output. The functionality ID must match one of the functionalities in your plain file.", ) + render_range_group.add_argument( + "--rerender", + type=frid_string, + help="Re-render a single already-rendered functionality (e.g. `2`). " + "Adds a new commit on top of the current HEAD without reverting any prior work. " + "Only top-level integer FRIDs are supported.", + ) parser.add_argument( "--force-render", diff --git a/render_machine/actions/commit_conformance_tests_changes.py b/render_machine/actions/commit_conformance_tests_changes.py index d029624..74fadbc 100644 --- a/render_machine/actions/commit_conformance_tests_changes.py +++ b/render_machine/actions/commit_conformance_tests_changes.py @@ -10,9 +10,8 @@ class CommitConformanceTestsChanges(BaseAction): SUCCESSFUL_OUTCOME_IMPLEMENTATION_NOT_UPDATED = "conformance_tests_changes_committed_implementation_not_updated" SUCCESSFUL_OUTCOME_IMPLEMENTATION_UPDATED = "conformance_tests_changes_committed_implementation_updated" - def __init__(self, implementation_code_commit_message: str, conformance_tests_commit_message: str): + def __init__(self, implementation_code_commit_message: str): self.implementation_code_commit_message = implementation_code_commit_message - self.conformance_tests_commit_message = conformance_tests_commit_message def execute(self, render_context: RenderContext, _previous_action_payload: Any | None): implementation_updated = False @@ -26,8 +25,13 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | ) implementation_updated = True + conformance_tests_commit_message = ( + git_utils.FUNCTIONAL_REQUIREMENT_REIMPLEMENTED_COMMIT_MESSAGE + if render_context.is_rerender + else git_utils.FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE + ) functional_requirement_text = render_context.frid_context.specifications[plain_spec.FUNCTIONAL_REQUIREMENTS][-1] - templated_functional_requirement_finished_commit_msg = self.conformance_tests_commit_message.format( + templated_functional_requirement_finished_commit_msg = conformance_tests_commit_message.format( render_context.frid_context.frid ) formatted_conformance_commit_msg = ( diff --git a/render_machine/actions/finish_functional_requirement.py b/render_machine/actions/finish_functional_requirement.py index 1a27f10..a445a88 100644 --- a/render_machine/actions/finish_functional_requirement.py +++ b/render_machine/actions/finish_functional_requirement.py @@ -1,14 +1,26 @@ from typing import Any -from render_machine.actions.commit_implementation_code_changes import CommitImplementationCodeChanges +import git_utils +from render_machine.actions.base_action import BaseAction from render_machine.render_context import RenderContext -class FinishFunctionalRequirement(CommitImplementationCodeChanges): +class FinishFunctionalRequirement(BaseAction): SUCCESSFUL_OUTCOME = "functional_requirement_finished" - def execute(self, render_context: RenderContext, previous_action_payload: Any | None): - super().execute(render_context, previous_action_payload) + def execute(self, render_context: RenderContext, _previous_action_payload: Any | None): + commit_message = ( + git_utils.FUNCTIONAL_REQUIREMENT_REIMPLEMENTED_COMMIT_MESSAGE + if render_context.is_rerender + else git_utils.FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE + ) + git_utils.add_all_files_and_commit( + render_context.build_folder, + commit_message.format(render_context.frid_context.frid), + render_context.module_name, + render_context.frid_context.frid, + render_context.run_state.render_id, + ) render_context.codeplain_api.finish_functional_requirement( render_context.frid_context.frid, diff --git a/render_machine/actions/prepare_repositories.py b/render_machine/actions/prepare_repositories.py index 4058728..0db5073 100644 --- a/render_machine/actions/prepare_repositories.py +++ b/render_machine/actions/prepare_repositories.py @@ -13,6 +13,9 @@ class PrepareRepositories(BaseAction): SUCCESSFUL_OUTCOME = "repositories_prepared" def execute(self, render_context: RenderContext, _previous_action_payload: Any | None): + if render_context.is_rerender: + return self._prepare_rerender(render_context) + if render_context.render_range is not None and render_context.render_range[0] != plain_spec.get_first_frid( render_context.plain_source_tree ): @@ -79,3 +82,47 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | ) return self.SUCCESSFUL_OUTCOME, None + + def _prepare_rerender(self, render_context: RenderContext): + frid = render_context.render_range[0] + + if "." in frid: + render_context.dispatch_error( + f"--rerender only supports top-level integer FRIDs (e.g. `1`, `2`). " + f"Nested FRID `{frid}` is not supported." + ) + return "error", None + + if not git_utils.has_commit_for_frid(render_context.build_folder, frid, render_context.module_name): + render_context.dispatch_error( + f"Cannot re-render functionality {frid} because it has not been fully rendered yet. " + f"Please render all functionalities first by running: " + f"codeplain {render_context.module_name}.plain" + ) + return "error", None + + render_context.starting_frid = frid + + module_metadata = render_context.plain_module.load_module_metadata() + if not module_metadata or "functionalities" not in module_metadata: + render_context.dispatch_error( + "module_metadata.json is missing or incomplete. " + "Please re-render the module from the beginning." + ) + return "error", None + + render_context.old_frid_spec = module_metadata["functionalities"][int(frid) - 1] + + if render_context.render_conformance_tests: + conformance_tests_json = render_context.conformance_tests.get_conformance_tests_json( + render_context.module_name + ) + if frid in conformance_tests_json: + old_folder = conformance_tests_json[frid]["folder_name"] + file_utils.delete_folder(old_folder) + del conformance_tests_json[frid] + render_context.conformance_tests.dump_conformance_tests_json( + render_context.module_name, conformance_tests_json + ) + + return self.SUCCESSFUL_OUTCOME, None diff --git a/render_machine/actions/render_conformance_tests.py b/render_machine/actions/render_conformance_tests.py index 178a971..38fed3b 100644 --- a/render_machine/actions/render_conformance_tests.py +++ b/render_machine/actions/render_conformance_tests.py @@ -126,6 +126,7 @@ def _render_conformance_tests(self, render_context: RenderContext): ), all_acceptance_tests, run_state=render_context.run_state, + is_reimplementation=render_context.is_rerender, ) render_context.conformance_tests_running_context.current_testing_frid_high_level_implementation_plan = ( diff --git a/render_machine/actions/render_functional_requirement.py b/render_machine/actions/render_functional_requirement.py index 35511bd..2590f5d 100644 --- a/render_machine/actions/render_functional_requirement.py +++ b/render_machine/actions/render_functional_requirement.py @@ -60,6 +60,8 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | render_context.get_required_modules_functionalities(), render_context.should_run_unit_tests(), render_context.run_state, + is_reimplementation=render_context.is_rerender, + old_functional_requirement_text=render_context.old_frid_spec, ) except FunctionalRequirementTooComplex as e: error_message = f"The functionality:\n{render_context.frid_context.functional_requirement_text}\n is too complex to be implemented. Please break down the functionality into smaller parts ({str(e)})." diff --git a/render_machine/render_context.py b/render_machine/render_context.py index ab8512e..15a7bb0 100644 --- a/render_machine/render_context.py +++ b/render_machine/render_context.py @@ -52,6 +52,7 @@ def __init__( test_script_timeout: Optional[int] = None, stop_event: Optional[threading.Event] = None, enter_pause_event: Optional[threading.Event] = None, + is_rerender: bool = False, ): self.codeplain_api: CodeplainAPI = codeplain_api self.memory_manager = memory_manager @@ -76,6 +77,8 @@ def __init__( self.event_bus = event_bus self.stop_event = stop_event self.enter_pause_event = enter_pause_event + self.is_rerender = is_rerender + self.old_frid_spec: str | None = None self.script_execution_history = ScriptExecutionHistory() self.starting_frid = None self.test_script_timeout = test_script_timeout diff --git a/render_machine/state_machine_config.py b/render_machine/state_machine_config.py index 147a539..43062da 100644 --- a/render_machine/state_machine_config.py +++ b/render_machine/state_machine_config.py @@ -55,12 +55,9 @@ def get_action_map(self) -> Dict[str, Any]: f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TESTS_READY_FOR_SUMMARY.value}": SummarizeConformanceTests(), f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TESTS_READY_FOR_COMMIT.value}": CommitConformanceTestsChanges( git_utils.CONFORMANCE_TESTS_PASSED_COMMIT_MESSAGE, - git_utils.FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE, ), f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TESTS_READY_FOR_AMBIGUITY_ANALYSIS.value}": AnalyzeSpecificationAmbiguity(), - f"{States.IMPLEMENTING_FRID.value}_{States.FRID_FULLY_IMPLEMENTED.value}": FinishFunctionalRequirement( - git_utils.FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE - ), + f"{States.IMPLEMENTING_FRID.value}_{States.FRID_FULLY_IMPLEMENTED.value}": FinishFunctionalRequirement(), f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.PROCESSING_UNIT_TESTS.value}_{States.UNIT_TESTS_READY.value}": RunUnitTests(), f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.PROCESSING_UNIT_TESTS.value}_{States.UNIT_TESTS_FAILED.value}": FixUnitTests(), States.RENDER_COMPLETED.value: CreateDist(), From 93482438c34a8f689ebc49c8d0e9b4b68ebb2630 Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Fri, 5 Jun 2026 15:23:55 +0200 Subject: [PATCH 03/11] commit module_metadata.json incrementally on each FRID update_frid_in_module_metadata writes the spec text for the just-finished FRID into .codeplain/module_metadata.json before the FRID checkpoint commit, so every git commit in the build repo contains an up-to-date functionalities array. This makes old spec text available to --rerender even after git clean. For a rerender it updates the existing slot in-place rather than appending. --- plain_modules.py | 15 +++++++++++++++ .../actions/finish_functional_requirement.py | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/plain_modules.py b/plain_modules.py index 1ed8da6..209abaf 100644 --- a/plain_modules.py +++ b/plain_modules.py @@ -108,6 +108,21 @@ def load_module_metadata(self) -> dict | None: with open(metadata_path, "r", encoding="utf-8") as f: return json.load(f) + def update_frid_in_module_metadata(self, frid: str, frid_text: str) -> None: + metadata = self.load_module_metadata() or {} + functionalities = metadata.get(MODULE_FUNCTIONALITIES, []) + frid_index = int(frid) - 1 + if frid_index < len(functionalities): + functionalities[frid_index] = frid_text + else: + functionalities.append(frid_text) + metadata[MODULE_FUNCTIONALITIES] = functionalities + + codeplain_folder = self.get_codeplain_folder() + os.makedirs(codeplain_folder, exist_ok=True) + with open(self.module_metadata_path(), "w", encoding="utf-8") as f: + json.dump(metadata, f, indent=4) + def get_module_source_hash(self) -> str: return plain_spec.get_hash_value([self.plain_source] + self.resources_list) diff --git a/render_machine/actions/finish_functional_requirement.py b/render_machine/actions/finish_functional_requirement.py index a445a88..cb17911 100644 --- a/render_machine/actions/finish_functional_requirement.py +++ b/render_machine/actions/finish_functional_requirement.py @@ -9,6 +9,11 @@ class FinishFunctionalRequirement(BaseAction): SUCCESSFUL_OUTCOME = "functional_requirement_finished" def execute(self, render_context: RenderContext, _previous_action_payload: Any | None): + render_context.plain_module.update_frid_in_module_metadata( + render_context.frid_context.frid, + render_context.frid_context.functional_requirement_text, + ) + commit_message = ( git_utils.FUNCTIONAL_REQUIREMENT_REIMPLEMENTED_COMMIT_MESSAGE if render_context.is_rerender From 6958e450bb66292a35040bb7eb3f41597c1ff68f Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Fri, 5 Jun 2026 15:32:28 +0200 Subject: [PATCH 04/11] skip revert_changes_for_frid during rerender --- render_machine/render_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/render_machine/render_utils.py b/render_machine/render_utils.py index 3d3e8e1..3b9d446 100644 --- a/render_machine/render_utils.py +++ b/render_machine/render_utils.py @@ -27,6 +27,8 @@ def revert_changes_for_frid(render_context): + if render_context.is_rerender: + return if render_context.frid_context.frid is not None: previous_frid = plain_spec.get_previous_frid(render_context.plain_source_tree, render_context.frid_context.frid) git_utils.revert_to_commit_with_frid(render_context.build_folder, previous_frid) From fdf271a8058ecbab38816d01c33f395130f2e1dc Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Fri, 5 Jun 2026 16:01:09 +0200 Subject: [PATCH 05/11] commit updated source_hash in module_metadata.json during rerender --- plain_modules.py | 4 +++- render_machine/actions/finish_functional_requirement.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plain_modules.py b/plain_modules.py index 209abaf..9b65cc7 100644 --- a/plain_modules.py +++ b/plain_modules.py @@ -108,7 +108,7 @@ def load_module_metadata(self) -> dict | None: with open(metadata_path, "r", encoding="utf-8") as f: return json.load(f) - def update_frid_in_module_metadata(self, frid: str, frid_text: str) -> None: + def update_frid_in_module_metadata(self, frid: str, frid_text: str, update_source_hash: bool = False) -> None: metadata = self.load_module_metadata() or {} functionalities = metadata.get(MODULE_FUNCTIONALITIES, []) frid_index = int(frid) - 1 @@ -117,6 +117,8 @@ def update_frid_in_module_metadata(self, frid: str, frid_text: str) -> None: else: functionalities.append(frid_text) metadata[MODULE_FUNCTIONALITIES] = functionalities + if update_source_hash: + metadata["source_hash"] = self.get_module_source_hash() codeplain_folder = self.get_codeplain_folder() os.makedirs(codeplain_folder, exist_ok=True) diff --git a/render_machine/actions/finish_functional_requirement.py b/render_machine/actions/finish_functional_requirement.py index cb17911..d71f92d 100644 --- a/render_machine/actions/finish_functional_requirement.py +++ b/render_machine/actions/finish_functional_requirement.py @@ -12,6 +12,7 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | render_context.plain_module.update_frid_in_module_metadata( render_context.frid_context.frid, render_context.frid_context.functional_requirement_text, + update_source_hash=render_context.is_rerender, ) commit_message = ( From ffa32e71fd779928be5f109470f68326fb949ec1 Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Mon, 8 Jun 2026 11:12:00 +0200 Subject: [PATCH 06/11] run conformance test regression through all FRIDs during rerender --- render_machine/render_context.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/render_machine/render_context.py b/render_machine/render_context.py index 15a7bb0..33430fa 100644 --- a/render_machine/render_context.py +++ b/render_machine/render_context.py @@ -382,11 +382,18 @@ def _get_next_test_to_run(self): def _has_reached_implementation_frid(self) -> bool: """Check if regression has reached the FRID being implemented.""" ctx = self.conformance_tests_running_context - return ( + if not ( ctx.execution_phase == TestExecutionPhase.RUNNING_REGRESSION and ctx.current_testing_module_name == self.module_name - and (ctx.current_testing_frid is None or ctx.current_testing_frid == ctx.frid_being_implemented) + ): + return False + + terminal_frid = ( + list(plain_spec.get_frids(self.plain_source_tree))[-1] + if self.is_rerender + else ctx.frid_being_implemented ) + return ctx.current_testing_frid is None or ctx.current_testing_frid == terminal_frid def _setup_test_specifications(self): """Load specifications for the current test.""" From 9b282b3cb72836180392a7d02bf8db7854977d03 Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Mon, 8 Jun 2026 11:44:47 +0200 Subject: [PATCH 07/11] skip rerendered FRID in conformance test regression loop --- render_machine/render_context.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/render_machine/render_context.py b/render_machine/render_context.py index 33430fa..986c47e 100644 --- a/render_machine/render_context.py +++ b/render_machine/render_context.py @@ -389,9 +389,7 @@ def _has_reached_implementation_frid(self) -> bool: return False terminal_frid = ( - list(plain_spec.get_frids(self.plain_source_tree))[-1] - if self.is_rerender - else ctx.frid_being_implemented + list(plain_spec.get_frids(self.plain_source_tree))[-1] if self.is_rerender else ctx.frid_being_implemented ) return ctx.current_testing_frid is None or ctx.current_testing_frid == terminal_frid @@ -513,6 +511,14 @@ def _handle_regression_testing(self): self._setup_test_specifications() if ctx.current_conformance_tests_exist(): + if self.is_rerender and ctx.current_testing_frid == ctx.frid_being_implemented: + # already tested in the initial phase — skip and advance to next + self.conformance_tests_running_context = self._get_next_test_to_run() + ctx = self.conformance_tests_running_context + self._setup_test_specifications() + if not ctx.current_conformance_tests_exist(): + return + # Check if this is the implementation FRID (last test to run) if self._has_reached_implementation_frid(): # Reached implementation FRID - only re-run it if code changed during regression @@ -528,6 +534,7 @@ def _handle_regression_testing(self): self.machine.dispatch(triggers.MARK_CONFORMANCE_TESTS_READY) + # TODO why is this in the context and not part of the state machine (in an action) # ========== Main Conformance Test Orchestration ========== def start_conformance_tests_for_frid(self): From 4e017c769913a0252c577a6601b787bbe4f68ad5 Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Mon, 8 Jun 2026 11:47:03 +0200 Subject: [PATCH 08/11] ignore worktrees --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7893c1e..3ea3be6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ dist .coverage logging_config.yaml + +.claude/worktrees/ \ No newline at end of file From dbd7f269e76efa55f77db1626214e82f339afd5f Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Mon, 8 Jun 2026 11:47:44 +0200 Subject: [PATCH 09/11] formatting --- render_machine/actions/prepare_repositories.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/render_machine/actions/prepare_repositories.py b/render_machine/actions/prepare_repositories.py index 0db5073..ee5dbd8 100644 --- a/render_machine/actions/prepare_repositories.py +++ b/render_machine/actions/prepare_repositories.py @@ -106,8 +106,7 @@ def _prepare_rerender(self, render_context: RenderContext): module_metadata = render_context.plain_module.load_module_metadata() if not module_metadata or "functionalities" not in module_metadata: render_context.dispatch_error( - "module_metadata.json is missing or incomplete. " - "Please re-render the module from the beginning." + "module_metadata.json is missing or incomplete. " "Please re-render the module from the beginning." ) return "error", None From f67a36d93c25cf3f715f92685920a63e801ea14e Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Mon, 8 Jun 2026 15:14:53 +0200 Subject: [PATCH 10/11] simpler documentation not revealing implementation too much --- plain2code_arguments.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plain2code_arguments.py b/plain2code_arguments.py index 1ca02d1..b4db934 100644 --- a/plain2code_arguments.py +++ b/plain2code_arguments.py @@ -230,7 +230,6 @@ def create_parser(): "--rerender", type=frid_string, help="Re-render a single already-rendered functionality (e.g. `2`). " - "Adds a new commit on top of the current HEAD without reverting any prior work. " "Only top-level integer FRIDs are supported.", ) From a3d613107c4ad72134ba8051e8899c566da87e7f Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Tue, 9 Jun 2026 10:48:35 +0200 Subject: [PATCH 11/11] get rid of on_enter hook and do the logic in the action instead --- render_machine/actions/prepare_testing_environment.py | 6 ++++++ render_machine/render_context.py | 8 -------- render_machine/state_machine_config.py | 5 +---- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/render_machine/actions/prepare_testing_environment.py b/render_machine/actions/prepare_testing_environment.py index 437187c..3798ebf 100644 --- a/render_machine/actions/prepare_testing_environment.py +++ b/render_machine/actions/prepare_testing_environment.py @@ -12,6 +12,12 @@ class PrepareTestingEnvironment(BaseAction): FAILED_OUTCOME = "testing_environment_preparation_failed" def execute(self, render_context: RenderContext, _previous_action_payload: Any | None): + if ( + render_context.prepare_environment_script is None + or not render_context.conformance_tests_running_context.should_prepare_testing_environment + ): + return self.SUCCESSFUL_OUTCOME, None + console.info( f"Running testing environment preparation script {render_context.prepare_environment_script} for build folder {render_context.build_folder}." ) diff --git a/render_machine/render_context.py b/render_machine/render_context.py index 986c47e..09eb22e 100644 --- a/render_machine/render_context.py +++ b/render_machine/render_context.py @@ -310,13 +310,6 @@ def start_refactoring_code(self): ) self.machine.dispatch(triggers.PROCEED_FRID_PROCESSING) - def start_testing_environment_preparation(self): - if ( - self.prepare_environment_script is None - or not self.conformance_tests_running_context.should_prepare_testing_environment - ): - self.machine.dispatch(triggers.MARK_TESTING_ENVIRONMENT_PREPARED) - def start_conformance_tests_processing(self): console.info("Implementing conformance tests...") current_frid_specifications, _ = plain_spec.get_specifications_for_frid( @@ -534,7 +527,6 @@ def _handle_regression_testing(self): self.machine.dispatch(triggers.MARK_CONFORMANCE_TESTS_READY) - # TODO why is this in the context and not part of the state machine (in an action) # ========== Main Conformance Test Orchestration ========== def start_conformance_tests_for_frid(self): diff --git a/render_machine/state_machine_config.py b/render_machine/state_machine_config.py index 43062da..58d5378 100644 --- a/render_machine/state_machine_config.py +++ b/render_machine/state_machine_config.py @@ -133,10 +133,7 @@ def get_processing_conformance_tests_states(self, render_context: RenderContext) "name": States.CONFORMANCE_TESTING_INITIALISED.value, "on_enter": render_context.start_conformance_tests_for_frid, }, - { - "name": States.CONFORMANCE_TEST_GENERATED.value, - "on_enter": render_context.start_testing_environment_preparation, - }, + States.CONFORMANCE_TEST_GENERATED.value, States.CONFORMANCE_TEST_ENV_PREPARED.value, { "name": States.CONFORMANCE_TEST_FAILED.value,