Skip to content

Commit f3ee187

Browse files
committed
Keep every fix round, and let both test loops see the other's
The memory for a functionality was rebuilt after every conformance test run and the previous one deleted, so a twenty round fix loop arrived at its last round remembering only the round the fixer can already see. It is now two journals with two lifetimes. The functionality journal in `.memory/attempts/` gains one file per round and touches no other, so what has been tried stays readable for the whole loop; global memory in `.memory/global_memory.json` holds what transfers beyond the functionality that learned it, and is written only by consolidation, which is the one place a recorded learning can be revised or retired. Global memory is copied forward to the next module rather than shared between them. A shared file would let a module inherit knowledge from its own future - re-render module three and it would read what modules four onwards learned in a later render - so each module's copy is a snapshot of what the chain knew when it was rendered, which keeps partial re-renders reproducible. Unit test rounds go into the same journal as conformance rounds, and the unit test fixer is given the memory, which it previously had none of. Inside a conformance fix loop it is also given the implementation changes that loop just made: a unit test asserting the behaviour one of them replaced looks exactly like evidence that the implementation is wrong, and repairing it there restores the conformance failure the change was made to fix. The journal is discarded when consolidation has taken what it needs from it, and again when a functionality starts, since by then its rounds describe a problem nobody is working on any more.
1 parent b710c29 commit f3ee187

16 files changed

Lines changed: 747 additions & 96 deletions

‎CLAUDE.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,9 @@ Two logging modes:
216216
- `<build-folder>/<module>/code/` — implementation code (its own git repo)
217217
- `<build-folder>/<module>/tests/` — conformance tests (its own git repo; only created when a conformance tests script is configured)
218218
- `<build-folder>/<module>/.codeplain/` — module metadata (`module_metadata.json`), not tracked in git
219-
- `<build-folder>/<module>/.memory/` — conformance test memory, not tracked in git
219+
- `<build-folder>/<module>/.memory/` — test-fixing memory, not tracked in git: `attempts/` holds the
220+
current functionality's fix journal (one file per round, append-only), and `global_memory.json` holds
221+
the condensed learnings, carried forward to the next module in the render chain
220222
- Each FRID render produces a git commit in the `code/` and `tests/` repos for rollback capability
221223

222224
### Configuration

‎codeplain_REST_api.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ def fix_unittests_issue(
236236
module_name: str,
237237
required_modules,
238238
unittests_issue,
239+
memory_files_content,
240+
conformance_fix_changes,
239241
run_state: RunState,
240242
):
241243
endpoint_url = f"{self.api_url}/fix_unittests_issue"
@@ -250,6 +252,8 @@ def fix_unittests_issue(
250252
"required_modules": required_modules,
251253
"unittests_issue": unittests_issue,
252254
"unittest_batch_id": run_state.unittest_batch_id,
255+
"memory_files_content": memory_files_content,
256+
"conformance_fix_changes": conformance_fix_changes,
253257
}
254258

255259
return self.post_request(endpoint_url, headers, payload, run_state)
@@ -269,6 +273,8 @@ def create_conformance_test_memory(
269273
conformance_tests_issue,
270274
conformance_tests_folder_name,
271275
previous_conformance_tests_issue_old,
276+
test_surface,
277+
attempt_file_name,
272278
run_state: RunState,
273279
):
274280
endpoint_url = f"{self.api_url}/create_conformance_test_memory"
@@ -288,6 +294,38 @@ def create_conformance_test_memory(
288294
"conformance_tests_issue": conformance_tests_issue,
289295
"conformance_tests_folder_name": conformance_tests_folder_name,
290296
"previous_conformance_tests_issue": previous_conformance_tests_issue_old,
297+
"test_surface": test_surface,
298+
"attempt_file_name": attempt_file_name,
299+
}
300+
301+
return self.post_request(endpoint_url, headers, payload, run_state)
302+
303+
def consolidate_global_memory(
304+
self,
305+
frid,
306+
plain_source_tree,
307+
linked_resources,
308+
memory_files_content,
309+
module_name,
310+
required_modules,
311+
conformance_tests_files,
312+
acceptance_tests,
313+
conformance_tests_folder_name,
314+
run_state: RunState,
315+
):
316+
endpoint_url = f"{self.api_url}/consolidate_global_memory"
317+
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
318+
319+
payload = {
320+
"frid": frid,
321+
"plain_source_tree": plain_source_tree,
322+
"linked_resources": linked_resources,
323+
"memory_files_content": memory_files_content,
324+
"module_name": module_name,
325+
"required_modules": required_modules,
326+
"conformance_tests_files": conformance_tests_files,
327+
"acceptance_tests": acceptance_tests,
328+
"conformance_tests_folder_name": conformance_tests_folder_name,
291329
}
292330

293331
return self.post_request(endpoint_url, headers, payload, run_state)

‎memory_management.py‎

Lines changed: 217 additions & 80 deletions
Large diffs are not rendered by default.

‎module_renderer.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ def __init__(
3737
self.stop_event = stop_event
3838
self.enter_pause_event = enter_pause_event
3939

40+
def _predecessor_memory_folder(self, plain_module: PlainModule) -> str | None:
41+
"""The memory folder of the module rendered immediately before this one, if there is one.
42+
43+
Taken from the module tree rather than from what has been rendered so far, so that the chain is the same
44+
whether or not a module was skipped as unchanged: a skipped module's global memory is still on disk and
45+
still belongs in the sequence.
46+
"""
47+
ordered: list[PlainModule] = []
48+
for module in plain_module.all_required_modules + [plain_module]:
49+
if all(seen.module_name != module.module_name for seen in ordered):
50+
ordered.append(module)
51+
52+
position = [module.module_name for module in ordered].index(plain_module.module_name)
53+
if position == 0:
54+
return None
55+
return ordered[position - 1].module_memory_folder
56+
4057
def _build_render_context_for_module(
4158
self,
4259
plain_module: PlainModule,
@@ -120,6 +137,7 @@ def _render_module(
120137
memory_manager = MemoryManager(
121138
self.codeplainAPI,
122139
plain_module.module_memory_folder,
140+
self._predecessor_memory_folder(plain_module),
123141
)
124142
render_context = self._build_render_context_for_module(
125143
plain_module,

‎plain_modules.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@
2424
import metadata_utils
2525
import plain_file
2626
import plain_spec
27-
from metadata_utils import (
28-
MODULE_FUNCTIONALITIES,
29-
MODULE_METADATA_FILENAME,
30-
REQUIRED_MODULES_FUNCTIONALITIES,
31-
)
27+
from metadata_utils import MODULE_FUNCTIONALITIES, MODULE_METADATA_FILENAME, REQUIRED_MODULES_FUNCTIONALITIES
3228
from plain2code_console import console
3329
from render_machine.implementation_code_helpers import ImplementationCodeHelpers
3430

‎render_machine/actions/fix_unit_tests.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from typing import Any
22

3+
import diff_utils
34
import file_utils
45
import render_machine.render_utils as render_utils
6+
from memory_management import MemoryManager
57
from plain2code_console import console
68
from plain2code_exceptions import InternalClientError
79
from render_machine.actions.base_action import BaseAction
@@ -14,6 +16,14 @@
1416
class FixUnitTests(BaseAction):
1517
SUCCESSFUL_OUTCOME = "unit_tests_fix_generated"
1618

19+
def __init__(self, inside_conformance_fix_loop: bool = False):
20+
"""``inside_conformance_fix_loop`` marks the instance that runs while :ConformanceTests: are being
21+
fixed. There, the implementation has just been changed on purpose to make a conformance test pass, so a
22+
unit test failing as a result is asserting behaviour that the change replaced - and repairing the
23+
implementation restores the conformance failure. The two loops then trade the same lines back and
24+
forth, neither able to see the other's history."""
25+
self.inside_conformance_fix_loop = inside_conformance_fix_loop
26+
1727
def execute(self, render_context: RenderContext, previous_action_payload: Any | None):
1828
if not previous_action_payload.get("previous_unittests_issue"):
1929
raise InternalClientError(
@@ -32,6 +42,12 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any |
3242

3343
render_utils.print_inputs(render_context, existing_files_content, "Files sent as input to unit tests fixing:")
3444

45+
_, memory_files_content = MemoryManager.fetch_memory_files(render_context.memory_manager.memory_folder)
46+
47+
conformance_fix_changes = None
48+
if self.inside_conformance_fix_loop:
49+
conformance_fix_changes = render_context.conformance_tests_running_context.code_diff_files
50+
3551
response_files = render_context.codeplain_api.fix_unittests_issue(
3652
render_context.frid_context.frid,
3753
render_context.plain_source_tree,
@@ -40,6 +56,8 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any |
4056
render_context.module_name,
4157
render_context.get_required_modules_functionalities(),
4258
previous_unittests_issue,
59+
memory_files_content,
60+
conformance_fix_changes,
4361
run_state=render_context.run_state,
4462
)
4563

@@ -49,6 +67,12 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any |
4967

5068
render_context.unit_tests_running_context.changed_files.update(changed_files)
5169

70+
# Held for the next unit test run to record: the failure this round answered, and what it changed.
71+
render_context.unit_tests_running_context.previous_unittests_issue = previous_unittests_issue
72+
render_context.unit_tests_running_context.code_diff_files = diff_utils.get_code_diff(
73+
response_files, existing_files_content
74+
)
75+
5276
console.print_files("Files fixed:", render_context.build_folder, response_files, style=console.OUTPUT_STYLE)
5377

5478
return self.SUCCESSFUL_OUTCOME, None

‎render_machine/actions/prepare_repositories.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any |
4040
file_utils.delete_folder(render_context.plain_module.module_folder)
4141
render_context.plain_module.seed_module_metadata()
4242

43+
# After the wipe, not before it: the module folder that was just deleted holds .memory, so global
44+
# memory has to be carried forward once there is somewhere for it to live.
45+
render_context.memory_manager.inherit_global_memory()
46+
4347
if render_context.required_modules:
4448
previous_module = render_context.required_modules[-1]
4549
console.debug(f"Cloning git repo from module {previous_module.module_name}.")

‎render_machine/actions/run_conformance_tests.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any |
5454
)
5555
render_context.script_execution_history.should_update_script_outputs = True
5656

57-
render_context.memory_manager.create_conformance_tests_memory(
57+
render_context.memory_manager.record_conformance_test_fix_attempt(
5858
render_context, exit_code, conformance_tests_issue
5959
)
6060

@@ -65,7 +65,7 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any |
6565
and render_context.conformance_tests_running_context.current_testing_frid
6666
== render_context.frid_context.frid
6767
):
68-
render_context.memory_manager.delete_unresolved_memory_files()
68+
render_context.memory_manager.consolidate_global_memory(render_context)
6969
return self.SUCCESSFUL_OUTCOME, None
7070

7171
if exit_code in UNRECOVERABLE_ERROR_EXIT_CODES:

‎render_machine/actions/run_unit_tests.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any |
3131

3232
render_context.script_execution_history.latest_unit_test_output_path = unittests_temp_log_file_path
3333
render_context.script_execution_history.should_update_script_outputs = True
34+
35+
# Recorded here rather than where the fix was made, because only now is the outcome of that fix known.
36+
render_context.memory_manager.record_unit_test_fix_attempt(render_context, exit_code, unittests_issue)
37+
3438
if exit_code == 0:
3539
return self.SUCCESSFUL_OUTCOME, None
3640

‎render_machine/render_context.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ def start_implementing_frid(self):
169169
functional_requirement_render_attempts=0,
170170
)
171171
self.run_state.current_frid = frid
172+
173+
# The journal describes rounds of fixing the previous functionality's tests. Those rounds are about a
174+
# problem that is no longer the one being worked on, and anything durable in them has already been
175+
# consolidated into global memory.
176+
self.memory_manager.clear_functionality_journal()
172177
return
173178

174179
def has_next_frid(self) -> bool:

0 commit comments

Comments
 (0)