codeplain 0.3.11.dev19__tar.gz → 0.3.11.dev21__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/PKG-INFO +1 -1
  2. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/codeplain_REST_api.py +26 -32
  3. codeplain-0.3.11.dev21/render_machine/actions/fix_unit_tests.py +256 -0
  4. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/run_unit_tests.py +9 -0
  5. codeplain-0.3.11.dev21/render_machine/agent_tools.py +270 -0
  6. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/render_context.py +9 -0
  7. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/render_types.py +45 -0
  8. codeplain-0.3.11.dev21/tests/test_agent_tools.py +166 -0
  9. codeplain-0.3.11.dev21/tests/test_fix_unit_tests_action.py +239 -0
  10. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_fix_unit_tests_conformance_context.py +120 -25
  11. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_tui_components.py +60 -1
  12. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tui/components.py +25 -2
  13. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tui/plain2code_tui.py +9 -0
  14. codeplain-0.3.11.dev19/render_machine/actions/fix_unit_tests.py +0 -79
  15. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.claude/settings.json +0 -0
  16. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.flake8 +0 -0
  17. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.github/dependabot.yml +0 -0
  18. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.github/workflows/e2e.yml +0 -0
  19. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.github/workflows/lint-and-test.yml +0 -0
  20. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.github/workflows/lint-powershell.yml +0 -0
  21. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.github/workflows/nofity-slack-on-main-merge.yml +0 -0
  22. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.github/workflows/publish-dev-to-pypi.yml +0 -0
  23. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.github/workflows/publish-install-script.yml +0 -0
  24. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.github/workflows/publish-to-pypi.yml +0 -0
  25. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.gitignore +0 -0
  26. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.vscode/launch.json +0 -0
  27. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/.vscode/settings.json +0 -0
  28. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/CLAUDE.md +0 -0
  29. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/CONTRIBUTING.md +0 -0
  30. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/LICENSE +0 -0
  31. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/README.md +0 -0
  32. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/change_detection.py +0 -0
  33. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/cli_output/__init__.py +0 -0
  34. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/cli_output/dry_run.py +0 -0
  35. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/cli_output/render_summary.py +0 -0
  36. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/cli_output/status.py +0 -0
  37. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/concept_utils.py +0 -0
  38. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/config/__init__.py +0 -0
  39. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/config/system_config.yaml +0 -0
  40. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/conformance_fix_journal.py +0 -0
  41. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/diff_utils.py +0 -0
  42. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/docs/generate_cli.py +0 -0
  43. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/docs/plain2code_cli.md +0 -0
  44. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/docs/starting_a_plain_project_from_scratch.md +0 -0
  45. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/event_bus.py +0 -0
  46. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_golang/config.yaml +0 -0
  47. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_golang/harness_tests/hello_world_test.go +0 -0
  48. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_golang/hello_world_golang.plain +0 -0
  49. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_golang/run.sh +0 -0
  50. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_python/config.yaml +0 -0
  51. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py +0 -0
  52. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_python/hello_world_python.plain +0 -0
  53. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_python/run.sh +0 -0
  54. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_react/config.yaml +0 -0
  55. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/e2e/hello_world.cy.ts +0 -0
  56. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/support/e2e.ts +0 -0
  57. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress.config.ts +0 -0
  58. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_react/harness_tests/hello_world_display/package.json +0 -0
  59. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_react/harness_tests/hello_world_display/tsconfig.json +0 -0
  60. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_react/hello_world_react.plain +0 -0
  61. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/example_hello_world_react/run.sh +0 -0
  62. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/examples/run.sh +0 -0
  63. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/file_utils.py +0 -0
  64. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/git_preflight.py +0 -0
  65. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/git_utils.py +0 -0
  66. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/install/bash/examples.sh +0 -0
  67. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/install/bash/install.sh +0 -0
  68. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/install/bash/walkthrough.sh +0 -0
  69. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/install/powershell/examples.ps1 +0 -0
  70. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/install/powershell/install.ps1 +0 -0
  71. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/install/powershell/walkthrough.ps1 +0 -0
  72. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/memory_management.py +0 -0
  73. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/metadata_utils.py +0 -0
  74. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/module_renderer.py +0 -0
  75. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/partial_rendering.py +0 -0
  76. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/path_resolution.py +0 -0
  77. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code.py +0 -0
  78. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_arguments.py +0 -0
  79. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_cli.md +0 -0
  80. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_console.py +0 -0
  81. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_events.py +0 -0
  82. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_exceptions.py +0 -0
  83. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_logger.py +0 -0
  84. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_nodes.py +0 -0
  85. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_read_config.py +0 -0
  86. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_state.py +0 -0
  87. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_telemetry.py +0 -0
  88. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain2code_utils.py +0 -0
  89. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain_file.py +0 -0
  90. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain_modules.py +0 -0
  91. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/plain_spec.py +0 -0
  92. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/pyproject.toml +0 -0
  93. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/pytest.ini +0 -0
  94. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/__init__.py +0 -0
  95. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/analyze_specification_ambiguity.py +0 -0
  96. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/base_action.py +0 -0
  97. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/commit_conformance_tests_changes.py +0 -0
  98. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/commit_implementation_code_changes.py +0 -0
  99. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/create_dist.py +0 -0
  100. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/distill_conformance_test_memory.py +0 -0
  101. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/exit_with_error.py +0 -0
  102. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/finish_functional_requirement.py +0 -0
  103. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/fix_conformance_test.py +0 -0
  104. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/prepare_repositories.py +0 -0
  105. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/prepare_testing_environment.py +0 -0
  106. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/refactor_code.py +0 -0
  107. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/render_conformance_tests.py +0 -0
  108. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/render_functional_requirement.py +0 -0
  109. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/run_conformance_tests.py +0 -0
  110. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/actions/summarize_conformance_tests.py +0 -0
  111. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/code_renderer.py +0 -0
  112. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/conformance_tests.py +0 -0
  113. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/implementation_code_helpers.py +0 -0
  114. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/render_utils.py +0 -0
  115. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/state_machine_config.py +0 -0
  116. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/states.py +0 -0
  117. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/render_machine/triggers.py +0 -0
  118. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/requirements.txt +0 -0
  119. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/resources/codeplain_overview.png +0 -0
  120. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/resources/plain_example.png +0 -0
  121. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/standard_template_library/golang-console-app-template.plain +0 -0
  122. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/standard_template_library/python-console-app-template.plain +0 -0
  123. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/standard_template_library/typescript-react-app-boilerplate.plain +0 -0
  124. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/standard_template_library/typescript-react-app-template.plain +0 -0
  125. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/system_config.py +0 -0
  126. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_conformance_tests_cypress.ps1 +0 -0
  127. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_conformance_tests_cypress.sh +0 -0
  128. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_conformance_tests_golang.ps1 +0 -0
  129. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_conformance_tests_golang.sh +0 -0
  130. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_conformance_tests_python.ps1 +0 -0
  131. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_conformance_tests_python.sh +0 -0
  132. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_unittests_flutter.ps1 +0 -0
  133. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_unittests_flutter.sh +0 -0
  134. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_unittests_golang.ps1 +0 -0
  135. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_unittests_golang.sh +0 -0
  136. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_unittests_python.ps1 +0 -0
  137. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_unittests_python.sh +0 -0
  138. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_unittests_react.ps1 +0 -0
  139. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/test_scripts/run_unittests_react.sh +0 -0
  140. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/__init__.py +0 -0
  141. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/conftest.py +0 -0
  142. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/acceptance_tests_warning/main_requiring_acceptance_tests.plain +0 -0
  143. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/acceptance_tests_warning/required_with_acceptance_tests.plain +0 -0
  144. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/circular_imports_1.plain +0 -0
  145. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/circular_imports_2.plain +0 -0
  146. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/circular_imports_main.plain +0 -0
  147. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/diamond_import_1.plain +0 -0
  148. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/diamond_import_2.plain +0 -0
  149. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/diamond_import_common.plain +0 -0
  150. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/diamond_imports_main.plain +0 -0
  151. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/import_with_functionalities.plain +0 -0
  152. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
  153. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/imports/non_existent_import.plain +0 -0
  154. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/partial_rendering/pr_code_var.plain +0 -0
  155. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/partial_rendering/pr_implement.plain +0 -0
  156. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/partial_rendering/pr_leaf.plain +0 -0
  157. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/partial_rendering/pr_middle.plain +0 -0
  158. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/partial_rendering/pr_root.plain +0 -0
  159. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/partial_rendering/pr_solo.plain +0 -0
  160. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  161. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  162. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
  163. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  164. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  165. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
  166. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  167. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/plain_source_with_missing_link.plain +0 -0
  168. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  169. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  170. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
  171. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  172. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  173. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  174. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  175. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  176. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  177. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
  178. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  179. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  180. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
  181. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  182. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  183. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
  184. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  185. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  186. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
  187. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  188. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
  189. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
  190. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
  191. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
  192. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
  193. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
  194. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
  195. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  196. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  197. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
  198. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
  199. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
  200. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  201. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  202. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  203. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  204. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  205. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  206. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/required_concepts.plain +0 -0
  207. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  208. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
  209. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  210. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  211. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  212. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  213. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  214. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  215. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
  216. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/topological_sort.plain +0 -0
  217. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  218. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/circular_requires_main.plain +0 -0
  219. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/circular_requires_sub.plain +0 -0
  220. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/diamond_requires_1.plain +0 -0
  221. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/diamond_requires_2.plain +0 -0
  222. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/diamond_requires_common.plain +0 -0
  223. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/diamond_requires_main.plain +0 -0
  224. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/independent_requires_1.plain +0 -0
  225. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/independent_requires_2.plain +0 -0
  226. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/independent_requires_main.plain +0 -0
  227. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/non_existent_require.plain +0 -0
  228. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/normal_requires_1.plain +0 -0
  229. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/normal_requires_2.plain +0 -0
  230. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/normal_requires_common.plain +0 -0
  231. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/requires/normal_requires_main.plain +0 -0
  232. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/sample_base64_image.txt +0 -0
  233. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/simple.plain +0 -0
  234. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/templates/block_level_include.plain +0 -0
  235. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/templates/code_variables.plain +0 -0
  236. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/templates/header.plain +0 -0
  237. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/templates/implement.plain +0 -0
  238. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/templates/implement_2.plain +0 -0
  239. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/templates/template_include.plain +0 -0
  240. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/data/templates/test_hardest_problem.plain +0 -0
  241. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/e2e/Dockerfile +0 -0
  242. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/e2e/conftest.py +0 -0
  243. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/e2e/test_hello_world_python.py +0 -0
  244. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/e2e/test_hello_world_python_windows.py +0 -0
  245. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_arg_provenance.py +0 -0
  246. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_change_detection.py +0 -0
  247. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_cli_output.py +0 -0
  248. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_conformance_fix_journal.py +0 -0
  249. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_console_log_styles.py +0 -0
  250. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_distill_conformance_test_memory.py +0 -0
  251. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_dry_run_link_validation.py +0 -0
  252. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_file_utils.py +0 -0
  253. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_folder_path_resolution.py +0 -0
  254. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_git_preflight.py +0 -0
  255. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_git_utils.py +0 -0
  256. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_headless_logging.py +0 -0
  257. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_imports.py +0 -0
  258. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_log_file_name_resolution.py +0 -0
  259. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_logging_config_path_resolution.py +0 -0
  260. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_partial_rendering.py +0 -0
  261. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_path_resolution.py +0 -0
  262. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_plain2code.py +0 -0
  263. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_plain2code_state.py +0 -0
  264. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_plain2code_utils.py +0 -0
  265. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_plain_modules.py +0 -0
  266. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_plainfile.py +0 -0
  267. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_plainfileparser.py +0 -0
  268. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_plainspec.py +0 -0
  269. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_prepare_repositories_layout.py +0 -0
  270. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_render_finished_report.py +0 -0
  271. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_requires.py +0 -0
  272. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_resolve_config_file.py +0 -0
  273. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_script_path_resolution.py +0 -0
  274. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_system_config.py +0 -0
  275. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_telemetry.py +0 -0
  276. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_tui_usage.py +0 -0
  277. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tests/test_usage_summary.py +0 -0
  278. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tui/__init__.py +0 -0
  279. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tui/models.py +0 -0
  280. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tui/plain_module_render_choice_tui.py +0 -0
  281. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tui/spinner.py +0 -0
  282. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tui/state_handlers.py +0 -0
  283. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tui/styles.css +0 -0
  284. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/tui/widget_helpers.py +0 -0
  285. {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev21}/usage_summary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeplain
3
- Version: 0.3.11.dev19
3
+ Version: 0.3.11.dev21
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -253,38 +253,6 @@ class CodeplainAPI:
253
253
 
254
254
  return self.post_request(endpoint_url, headers, payload, run_state)
255
255
 
256
- def fix_unittests_issue(
257
- self,
258
- frid,
259
- plain_source_tree,
260
- linked_resources,
261
- existing_files_content,
262
- module_name: str,
263
- required_modules,
264
- unittests_issue,
265
- run_state: RunState,
266
- conformance_tests_fixes: list[dict] | None = None,
267
- ):
268
- endpoint_url = f"{self.api_url}/fix_unittests_issue"
269
- headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
270
-
271
- payload = {
272
- "frid": frid,
273
- "plain_source_tree": plain_source_tree,
274
- "linked_resources": linked_resources,
275
- "existing_files_content": existing_files_content,
276
- "module_name": module_name,
277
- "required_modules": required_modules,
278
- "unittests_issue": unittests_issue,
279
- "unittest_batch_id": run_state.unittest_batch_id,
280
- }
281
- # Implementation code changes made by the conformance tests fixer right before this unit tests run;
282
- # only sent when unit tests are processed inside the conformance tests phase.
283
- if conformance_tests_fixes is not None:
284
- payload["conformance_tests_fixes"] = conformance_tests_fixes
285
-
286
- return self.post_request(endpoint_url, headers, payload, run_state)
287
-
288
256
  def distill_conformance_test_memory(
289
257
  self,
290
258
  frid,
@@ -558,3 +526,29 @@ class CodeplainAPI:
558
526
  }
559
527
 
560
528
  return self.post_request(endpoint_url, headers, payload, run_state)
529
+
530
+ def agent_start(self, task_type: str, task_params: dict, frid: str, module_name: str, run_state: RunState):
531
+ """Start a server-side agent session; returns the first turn (tool calls or completion)."""
532
+ endpoint_url = f"{self.api_url}/agent/start"
533
+ headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
534
+ payload = {
535
+ "task_type": task_type,
536
+ "task_params": task_params,
537
+ "frid": frid,
538
+ "module_name": module_name,
539
+ }
540
+ return self.post_request(endpoint_url, headers, payload, run_state)
541
+
542
+ def agent_continue(
543
+ self, session_id: str, tool_results: list[dict], frid: str, module_name: str, run_state: RunState
544
+ ):
545
+ """Feed tool results into an agent session and run its next turn."""
546
+ endpoint_url = f"{self.api_url}/agent/continue"
547
+ headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
548
+ payload = {
549
+ "session_id": session_id,
550
+ "tool_results": tool_results,
551
+ "frid": frid,
552
+ "module_name": module_name,
553
+ }
554
+ return self.post_request(endpoint_url, headers, payload, run_state)
@@ -0,0 +1,256 @@
1
+ import os
2
+ from http import HTTPStatus
3
+ from typing import Any
4
+
5
+ import requests
6
+
7
+ import file_utils
8
+ import plain_spec
9
+ from plain2code_console import console
10
+ from plain2code_exceptions import InternalClientError
11
+ from render_machine import agent_tools
12
+ from render_machine.actions.base_action import BaseAction
13
+ from render_machine.render_context import RenderContext
14
+
15
+ TASK_TYPE = "fix_unit_tests"
16
+ SUBMIT_FIX_TOOL = "submit_fix"
17
+ # Upper bound on LLM turns spent on one fix attempt; the server bounds the whole session.
18
+ MAX_AGENT_TURNS_PER_ATTEMPT = 40
19
+ # Answer to a submit_fix whose fix was accepted in an earlier unit-test loop of the conformance phase.
20
+ ACCEPTED_THEN_CHANGED_MESSAGE = (
21
+ "Your fix was accepted: the unit tests passed. Afterwards the implementation code was changed to fix the "
22
+ "conformance tests{see_below}, and the unit tests now fail again. Files may have changed since you last read "
23
+ "them."
24
+ )
25
+ # Seeding the first turn: the build folder's file list and the files changed for the FRID.
26
+ MAX_FILE_TREE_ENTRIES = 500
27
+ MAX_RELEVANT_FILES_CHARS = 60_000
28
+
29
+
30
+ class FixUnitTests(BaseAction):
31
+ """Fix failing unit tests with a server-side agent session that spans the fix attempts.
32
+
33
+ The first failure starts a session; the agent then drives read/grep/edit/run tool calls
34
+ (executed here) until it calls submit_fix. The state machine re-runs the unit tests and,
35
+ if they still fail, the next execution of this action answers that submit_fix call with
36
+ the new failure output inside the same session, so earlier attempts stay in context.
37
+
38
+ During the conformance phase the session also spans unit-test loops: after the conformance
39
+ tests fixer changes implementation code, the unit tests fail again and the same session is
40
+ continued, told what the conformance tests fixer changed (which it must preserve).
41
+ """
42
+
43
+ SUCCESSFUL_OUTCOME = "unit_tests_fix_generated"
44
+
45
+ def execute(self, render_context: RenderContext, previous_action_payload: Any | None):
46
+ if not previous_action_payload or not previous_action_payload.get("previous_unittests_issue"):
47
+ raise InternalClientError(
48
+ "Internal client error: Previous action payload does not contain previous unit tests issue."
49
+ )
50
+ unittests_issue = previous_action_payload["previous_unittests_issue"]
51
+ context = render_context.unit_tests_running_context
52
+ session = render_context.unit_tests_agent_session
53
+ api = render_context.codeplain_api
54
+ frid, module_name = render_context.frid_context.frid, render_context.module_name
55
+ changed_files_before = set(context.changed_files)
56
+ log_path = render_context.script_execution_history.latest_unit_test_output_path
57
+ if log_path:
58
+ agent_tools.register_log_path(log_path, render_context)
59
+ conformance_tests_fixes = self._get_conformance_tests_fixes(render_context)
60
+ new_conformance_tests_fixes = conformance_tests_fixes[session.conformance_fixes_handed_off :]
61
+ if new_conformance_tests_fixes:
62
+ console.info(
63
+ f"Unit tests are fixed while preserving {len(new_conformance_tests_fixes)} implementation code "
64
+ "change(s) made to fix the conformance tests."
65
+ )
66
+
67
+ response = None
68
+ if session.session_id is not None:
69
+ if context.agent_used_in_this_loop:
70
+ console.info(f"Continuing agent session {session.session_id} with the new unit tests failure.")
71
+ output = "The fix was applied, but the unit tests still fail."
72
+ else:
73
+ # The session's last fix was accepted in an earlier unit-test loop of this conformance
74
+ # phase; since then the conformance tests fixer changed the code and the tests fail again.
75
+ console.info(
76
+ f"Continuing agent session {session.session_id}: the unit tests fail again after the "
77
+ "implementation was changed to fix the conformance tests."
78
+ )
79
+ output = ACCEPTED_THEN_CHANGED_MESSAGE.format(
80
+ see_below=" (see the Conformance Tests Fix below)" if new_conformance_tests_fixes else ""
81
+ )
82
+ # Files changed outside the session, so earlier read results are stale.
83
+ context.tool_result_cache.clear()
84
+ submit_result: dict = {
85
+ "call_id": session.pending_submit_call_id,
86
+ "output": output + agent_tools.full_log_pointer(log_path),
87
+ "test_output": unittests_issue,
88
+ }
89
+ if new_conformance_tests_fixes:
90
+ submit_result["conformance_tests_fixes"] = new_conformance_tests_fixes
91
+ tool_results = session.pending_tool_results + [submit_result]
92
+ session.pending_tool_results, session.pending_submit_call_id = [], None
93
+ response = self._continue_session(render_context, session.session_id, tool_results)
94
+ if response is None:
95
+ console.info("Starting an agent session to fix the unit tests.")
96
+ # Cached read results point at earlier turns, which a new session does not have.
97
+ context.tool_result_cache.clear()
98
+ response = api.agent_start(
99
+ TASK_TYPE,
100
+ self._build_task_params(render_context, unittests_issue, conformance_tests_fixes),
101
+ frid,
102
+ module_name,
103
+ render_context.run_state,
104
+ )
105
+ session.session_id = response["session_id"]
106
+ assert session.session_id is not None
107
+ session.conformance_fixes_handed_off = len(conformance_tests_fixes)
108
+ context.agent_used_in_this_loop = True
109
+
110
+ submitted = False
111
+ turns = 0
112
+ while response.get("status") == "tool_calls" and turns < MAX_AGENT_TURNS_PER_ATTEMPT:
113
+ turns += 1
114
+ calls = response["calls"]
115
+ submit_call = next((call for call in calls if call["name"] == SUBMIT_FIX_TOOL), None)
116
+ tool_results = agent_tools.execute_calls(
117
+ [call for call in calls if call is not submit_call], render_context
118
+ )
119
+ if submit_call is not None:
120
+ session.pending_submit_call_id = submit_call["id"]
121
+ session.pending_tool_results = tool_results
122
+ submitted = True
123
+ console.info(f"Agent submitted a fix: {submit_call['args'].get('changes_made', '')}")
124
+ break
125
+ response = api.agent_continue(session.session_id, tool_results, frid, module_name, render_context.run_state)
126
+
127
+ if not submitted:
128
+ # The session ended without a submission (finished in text, failed, or used up this
129
+ # attempt's turn budget) — a fresh session is started if the tests still fail.
130
+ status = response.get("status")
131
+ if status == "failed":
132
+ console.warning(f"Agent session failed: {response.get('error', 'unknown error')}")
133
+ elif status == "tool_calls":
134
+ console.warning(f"Agent used {MAX_AGENT_TURNS_PER_ATTEMPT} turns without submitting a fix.")
135
+ session.reset()
136
+
137
+ console.print_files(
138
+ "Files changed while fixing unit tests:",
139
+ render_context.build_folder,
140
+ {path: "" for path in sorted(context.changed_files - changed_files_before)},
141
+ style=console.OUTPUT_STYLE,
142
+ )
143
+ return self.SUCCESSFUL_OUTCOME, None
144
+
145
+ @staticmethod
146
+ def _get_conformance_tests_fixes(render_context: RenderContext) -> list[dict]:
147
+ """Implementation code changes the conformance tests fixer made during this conformance phase, oldest
148
+ first. Empty outside the conformance phase (the implementation and refactoring unit-test loops)."""
149
+ conformance_tests_running_context = getattr(render_context, "conformance_tests_running_context", None)
150
+ if conformance_tests_running_context is None:
151
+ return []
152
+ return list(getattr(conformance_tests_running_context, "implementation_code_fixes", None) or [])
153
+
154
+ @staticmethod
155
+ def _continue_session(render_context: RenderContext, session_id: str, tool_results: list[dict]) -> dict | None:
156
+ """Continue the session; None if the server no longer has it (expired), so a new one is started."""
157
+ try:
158
+ return render_context.codeplain_api.agent_continue(
159
+ session_id,
160
+ tool_results,
161
+ render_context.frid_context.frid,
162
+ render_context.module_name,
163
+ render_context.run_state,
164
+ )
165
+ except requests.exceptions.HTTPError as e:
166
+ if e.response is None or e.response.status_code != HTTPStatus.NOT_FOUND:
167
+ raise
168
+ console.warning(f"Agent session {session_id} has expired; starting a new one.")
169
+ render_context.unit_tests_agent_session.reset()
170
+ return None
171
+
172
+ @staticmethod
173
+ def _build_task_params(
174
+ render_context: RenderContext, unittests_issue: str, conformance_tests_fixes: list[dict]
175
+ ) -> dict:
176
+ frid = render_context.frid_context.frid
177
+ specifications, _ = plain_spec.get_specifications_for_frid(render_context.plain_source_tree, frid)
178
+ context = render_context.unit_tests_running_context
179
+ # The files the conformance tests fixes changed are always seeded, so the agent sees what to preserve.
180
+ conformance_fix_files = {name for fix in conformance_tests_fixes for name in (fix or {}).get("code_diff") or {}}
181
+ task_params = {
182
+ "definitions": "\n".join(specifications.get(plain_spec.DEFINITIONS, [])),
183
+ "non_functional_requirements": "\n".join(specifications.get(plain_spec.NON_FUNCTIONAL_REQUIREMENTS, [])),
184
+ "functional_requirements": FixUnitTests._functional_requirements_section(render_context, specifications),
185
+ "linked_resources": render_context.frid_context.linked_resources,
186
+ "build_folder": render_context.build_folder,
187
+ "module_name": render_context.module_name,
188
+ "unittests_script_content": FixUnitTests._read_script(render_context.unittests_script),
189
+ "unittests_issue": unittests_issue,
190
+ "unittests_log_path": render_context.script_execution_history.latest_unit_test_output_path,
191
+ "file_tree": FixUnitTests._file_tree(render_context.build_folder),
192
+ "relevant_files": FixUnitTests._relevant_files(
193
+ render_context.build_folder,
194
+ render_context.frid_context.changed_files | context.changed_files | conformance_fix_files,
195
+ ),
196
+ }
197
+ if conformance_tests_fixes:
198
+ task_params["conformance_tests_fixes"] = conformance_tests_fixes
199
+ session = render_context.unit_tests_agent_session
200
+ if session.previous_session_id:
201
+ task_params["previous_session_id"] = session.previous_session_id
202
+ return task_params
203
+
204
+ @staticmethod
205
+ def _file_tree(build_folder: str) -> str:
206
+ paths = []
207
+ for root, dirs, files in os.walk(build_folder):
208
+ dirs[:] = sorted(d for d in dirs if d not in agent_tools.GREP_EXCLUDED_DIRS and not d.startswith("."))
209
+ paths.extend(os.path.relpath(os.path.join(root, name), build_folder) for name in sorted(files))
210
+ if len(paths) > MAX_FILE_TREE_ENTRIES:
211
+ return "\n".join(paths[:MAX_FILE_TREE_ENTRIES]) + "\n... [more files not listed]"
212
+ return "\n".join(paths)
213
+
214
+ @staticmethod
215
+ def _relevant_files(build_folder: str, file_names: set[str]) -> dict[str, str]:
216
+ """Contents of the given build-relative files, smallest first, within MAX_RELEVANT_FILES_CHARS."""
217
+ contents = {}
218
+ for name in file_names:
219
+ full_path = os.path.join(build_folder, name)
220
+ if os.path.isfile(full_path):
221
+ with open(full_path, "r", encoding="utf-8", errors="replace") as f:
222
+ contents[name] = f.read()
223
+ relevant, total = {}, 0
224
+ for name in sorted(contents, key=lambda n: (len(contents[n]), n)):
225
+ if total + len(contents[name]) > MAX_RELEVANT_FILES_CHARS:
226
+ break
227
+ relevant[name] = contents[name]
228
+ total += len(contents[name])
229
+ return dict(sorted(relevant.items()))
230
+
231
+ @staticmethod
232
+ def _functional_requirements_section(render_context: RenderContext, specifications: dict) -> str:
233
+ sections = []
234
+ for module_name, functionalities in render_context.get_required_modules_functionalities().items():
235
+ sections.append(
236
+ f"### Module: {module_name} (Already Implemented, for context)\n" + "\n".join(functionalities)
237
+ )
238
+ current = specifications.get(plain_spec.FUNCTIONAL_REQUIREMENTS, [])
239
+ if len(current) > 1:
240
+ sections.append(
241
+ f"### Module: {render_context.module_name} (Already Implemented, for context)\n"
242
+ + "\n".join(current[:-1])
243
+ )
244
+ if current:
245
+ sections.append(f"### Module: {render_context.module_name} (Currently Being Implemented)\n{current[-1]}")
246
+ return "\n\n".join(sections)
247
+
248
+ @staticmethod
249
+ def _read_script(script: str | None) -> str:
250
+ if not script:
251
+ return ""
252
+ try:
253
+ with open(file_utils.add_current_path_if_no_path(script), "r", encoding="utf-8") as f:
254
+ return f.read()
255
+ except OSError:
256
+ return ""
@@ -16,6 +16,15 @@ class RunUnitTests(BaseAction):
16
16
  UNRECOVERABLE_ERROR_OUTCOME = "unrecoverable_error_occurred"
17
17
 
18
18
  def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
19
+ context = render_context.unit_tests_running_context
20
+ if context.verified_passing:
21
+ # The fixing agent's own run passed and no file changed since; running again is redundant.
22
+ context.verified_passing = False
23
+ console.info("Unit tests already passed in the fixing agent's last run; not running them again.")
24
+ render_context.script_execution_history.latest_unit_test_output_path = context.verified_passing_log_path
25
+ render_context.script_execution_history.should_update_script_outputs = True
26
+ return self.SUCCESSFUL_OUTCOME, None
27
+
19
28
  unittests_script = os.path.normpath(render_context.unittests_script)
20
29
 
21
30
  console.info(
@@ -0,0 +1,270 @@
1
+ """Client-side implementations of the tools a server-side agent can call.
2
+
3
+ The server declares the tools to the LLM (codeplain-api: src/agent/tools.py) and forwards
4
+ the model's calls; this module executes them against the local build folder and returns
5
+ plain-text results. Relative paths resolve against the build folder. Reads are allowed only in the
6
+ build folder and the full test logs the agent was pointed to; writes only inside the build folder.
7
+ """
8
+
9
+ import glob
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import tempfile
14
+ from typing import Callable
15
+
16
+ from plain2code_console import console
17
+ from render_machine import render_utils
18
+ from render_machine.render_context import RenderContext
19
+
20
+ DEFAULT_READ_LIMIT = 200
21
+ MAX_LINE_CHARS = 10_000
22
+ MAX_OUTPUT_CHARS = 30_000
23
+ EDIT_SNIPPET_CONTEXT_LINES = 3
24
+ MAX_EDIT_SNIPPET_LINES = 60
25
+ MAX_GREP_CONTEXT_LINES = 20
26
+ # Tools without side effects; repeating one while no file changed returns a pointer instead.
27
+ READ_ONLY_TOOLS = ("read_file", "grep", "ls_files")
28
+ GREP_EXCLUDED_DIRS = (".git", "__pycache__", "node_modules", ".venv", "target", "dist", "build")
29
+
30
+
31
+ def _build_folder(render_context: RenderContext) -> str:
32
+ return os.path.normpath(os.path.abspath(render_context.build_folder))
33
+
34
+
35
+ def _resolve(file_path: str, render_context: RenderContext) -> str:
36
+ if os.path.isabs(file_path):
37
+ return os.path.normpath(file_path)
38
+ return os.path.normpath(os.path.join(_build_folder(render_context), file_path))
39
+
40
+
41
+ def _within(path: str, folder: str) -> bool:
42
+ return path == folder or path.startswith(folder + os.sep)
43
+
44
+
45
+ def _readable(path: str, render_context: RenderContext) -> bool:
46
+ return (
47
+ _within(path, _build_folder(render_context))
48
+ or path in render_context.unit_tests_agent_session.readable_log_paths
49
+ )
50
+
51
+
52
+ def register_log_path(log_path: str, render_context: RenderContext) -> None:
53
+ """Allow read_file/grep on a full test log the agent is pointed to."""
54
+ render_context.unit_tests_agent_session.readable_log_paths.add(os.path.normpath(os.path.abspath(log_path)))
55
+
56
+
57
+ def _writable(path: str, render_context: RenderContext) -> bool:
58
+ return _within(path, _build_folder(render_context))
59
+
60
+
61
+ def _bound(text: str) -> str:
62
+ """Cap very long lines and the total size so one tool result cannot flood the context."""
63
+ lines = [
64
+ line if len(line) <= MAX_LINE_CHARS else line[:MAX_LINE_CHARS] + "... [line truncated]"
65
+ for line in text.split("\n")
66
+ ]
67
+ text = "\n".join(lines)
68
+ if len(text) > MAX_OUTPUT_CHARS:
69
+ head, tail = int(MAX_OUTPUT_CHARS * 0.6), int(MAX_OUTPUT_CHARS * 0.4)
70
+ text = text[:head] + f"\n\n... [truncated {len(text) - head - tail:,} chars] ...\n\n" + text[-tail:]
71
+ return text
72
+
73
+
74
+ def _track_change(full_path: str, render_context: RenderContext) -> None:
75
+ context = render_context.unit_tests_running_context
76
+ context.changed_files.add(os.path.relpath(full_path, _build_folder(render_context)))
77
+ context.verified_passing, context.verified_passing_log_path = False, None
78
+ context.tool_result_cache.clear()
79
+
80
+
81
+ def read_file(args: dict, render_context: RenderContext) -> str:
82
+ full_path = _resolve(args.get("file_path", ""), render_context)
83
+ if not _readable(full_path, render_context):
84
+ return f"Error: read access denied for '{full_path}' (readable: build folder and test logs)."
85
+ if not os.path.isfile(full_path):
86
+ return f"Error: file not found: '{full_path}'."
87
+ with open(full_path, "r", encoding="utf-8", errors="replace") as f:
88
+ lines = f.read().split("\n")
89
+ offset = max(int(args.get("offset") or 1), 1)
90
+ limit = int(args.get("limit") or DEFAULT_READ_LIMIT)
91
+ selected = lines[offset - 1 : offset - 1 + limit]
92
+ if not selected:
93
+ return f"Error: offset {offset} is past the end of the file ({len(lines)} lines)."
94
+ numbered = "\n".join(f"{offset + i}: {line}" for i, line in enumerate(selected))
95
+ last = offset - 1 + len(selected)
96
+ note = (
97
+ f"\n[showing lines {offset}-{last} of {len(lines)}; use offset={last + 1} to continue]"
98
+ if last < len(lines)
99
+ else ""
100
+ )
101
+ return _bound(numbered) + note
102
+
103
+
104
+ def grep(args: dict, render_context: RenderContext) -> str:
105
+ pattern = args.get("pattern", "")
106
+ if not pattern:
107
+ return "Error: pattern is required."
108
+ target = _resolve(args.get("file_path") or ".", render_context)
109
+ if not _readable(target, render_context):
110
+ return f"Error: read access denied for '{target}'."
111
+ if not os.path.exists(target):
112
+ return f"Error: path not found: '{target}'."
113
+ # Run from the build folder so matches inside it come back as build-relative paths, which
114
+ # is the form the other tools accept; a test log outside it is passed by absolute path.
115
+ cwd = _build_folder(render_context)
116
+ options = [f"--exclude-dir={d}" for d in GREP_EXCLUDED_DIRS]
117
+ context_lines = min(max(int(args.get("context_lines") or 0), 0), MAX_GREP_CONTEXT_LINES)
118
+ if context_lines:
119
+ options.append(f"-C{context_lines}")
120
+ if args.get("include"):
121
+ options.append(f"--include={args['include']}")
122
+ command = ["grep", "-rnI", *options, "-e", pattern, "--"]
123
+ command.append(os.path.relpath(target, cwd) if _within(target, cwd) else target)
124
+ result = subprocess.run(command, capture_output=True, text=True, cwd=cwd)
125
+ if result.returncode == 1:
126
+ return f"No matches for '{pattern}' in '{target}'."
127
+ if result.returncode != 0:
128
+ return f"Error: grep failed: {result.stderr.strip()}"
129
+ lines = [line[2:] if line.startswith("./") else line for line in result.stdout.rstrip("\n").split("\n")]
130
+ return _bound("\n".join(lines))
131
+
132
+
133
+ def ls_files(args: dict, render_context: RenderContext) -> str:
134
+ target = _resolve(args.get("pattern") or ".", render_context)
135
+ if not _readable(target, render_context):
136
+ return f"Error: read access denied for '{target}'."
137
+ if os.path.isdir(target):
138
+ entries = sorted(os.listdir(target))
139
+ listing = [entry + "/" if os.path.isdir(os.path.join(target, entry)) else entry for entry in entries]
140
+ return f"{target}:\n" + ("\n".join(listing) if listing else "(empty)")
141
+ matches = sorted(glob.glob(target, recursive=True))
142
+ return _bound("\n".join(matches)) if matches else f"No files match '{target}'."
143
+
144
+
145
+ def edit_file(args: dict, render_context: RenderContext) -> str:
146
+ full_path = _resolve(args.get("file_path", ""), render_context)
147
+ search, replace = args.get("search", ""), args.get("replace", "")
148
+ if not search:
149
+ return "Error: search is required."
150
+ if not _writable(full_path, render_context):
151
+ return f"Error: write access denied for '{full_path}' (writable: build folder only)."
152
+ if not os.path.isfile(full_path):
153
+ return f"Error: file not found: '{full_path}'. Use write_file to create new files."
154
+ with open(full_path, "r", encoding="utf-8") as f:
155
+ content = f.read()
156
+ occurrences = content.count(search)
157
+ if occurrences != 1:
158
+ return (
159
+ f"Error: search text found {occurrences} times in '{full_path}'; it must appear exactly once. "
160
+ "Read the file and use a larger, unique snippet."
161
+ )
162
+ with open(full_path, "w", encoding="utf-8") as f:
163
+ f.write(content.replace(search, replace, 1))
164
+ _track_change(full_path, render_context)
165
+ return f"Edited '{full_path}'. The edited region now reads:\n" + _edit_snippet(content, search, replace)
166
+
167
+
168
+ def _edit_snippet(content: str, search: str, replace: str) -> str:
169
+ """Numbered lines of the replacement plus a little context, so the edit needs no re-read."""
170
+ lines = content.replace(search, replace, 1).split("\n")
171
+ first = content[: content.index(search)].count("\n")
172
+ last = first + replace.count("\n")
173
+ start = max(first - EDIT_SNIPPET_CONTEXT_LINES, 0)
174
+ end = min(last + EDIT_SNIPPET_CONTEXT_LINES + 1, len(lines), start + MAX_EDIT_SNIPPET_LINES)
175
+ snippet = "\n".join(f"{start + i + 1}: {line}" for i, line in enumerate(lines[start:end]))
176
+ return _bound(snippet)
177
+
178
+
179
+ def write_file(args: dict, render_context: RenderContext) -> str:
180
+ full_path = _resolve(args.get("file_path", ""), render_context)
181
+ if not _writable(full_path, render_context):
182
+ return f"Error: write access denied for '{full_path}' (writable: build folder only)."
183
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
184
+ with open(full_path, "w", encoding="utf-8") as f:
185
+ f.write(args.get("content", ""))
186
+ _track_change(full_path, render_context)
187
+ return f"Wrote '{full_path}'."
188
+
189
+
190
+ def delete_file(args: dict, render_context: RenderContext) -> str:
191
+ full_path = _resolve(args.get("file_path", ""), render_context)
192
+ if not _writable(full_path, render_context):
193
+ return f"Error: write access denied for '{full_path}' (writable: build folder only)."
194
+ if not os.path.isfile(full_path):
195
+ return f"Error: file not found: '{full_path}'."
196
+ os.remove(full_path)
197
+ _track_change(full_path, render_context)
198
+ return f"Deleted '{full_path}'."
199
+
200
+
201
+ def full_log_pointer(log_file_path: str | None) -> str:
202
+ return f" Full log: {log_file_path} (search it with grep, passing that path as file_path)." if log_file_path else ""
203
+
204
+
205
+ def run_unit_tests(_args: dict, render_context: RenderContext) -> dict:
206
+ """Returns a result dict: a short header in `output` and the raw failure output in
207
+ `test_output`, which the server condenses (summarizing it when it is long)."""
208
+ exit_code, output, log_file_path = render_utils.execute_script(
209
+ os.path.normpath(render_context.unittests_script),
210
+ [render_context.build_folder],
211
+ "Unit Tests",
212
+ timeout=render_context.test_script_timeout,
213
+ stop_event=render_context.stop_event,
214
+ )
215
+ context = render_context.unit_tests_running_context
216
+ if exit_code == 0:
217
+ context.verified_passing, context.verified_passing_log_path = True, log_file_path
218
+ return {"output": "All unit tests passed."}
219
+ if not log_file_path and output:
220
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, suffix=".unittest_output") as f:
221
+ f.write(output)
222
+ log_file_path = f.name
223
+ if log_file_path:
224
+ register_log_path(log_file_path, render_context)
225
+ return {
226
+ "output": f"Unit tests failed (exit code {exit_code}).{full_log_pointer(log_file_path)}",
227
+ "test_output": output,
228
+ }
229
+
230
+
231
+ TOOLS: dict[str, Callable[[dict, RenderContext], str | dict]] = {
232
+ "read_file": read_file,
233
+ "grep": grep,
234
+ "ls_files": ls_files,
235
+ "edit_file": edit_file,
236
+ "write_file": write_file,
237
+ "delete_file": delete_file,
238
+ "run_unit_tests": run_unit_tests,
239
+ }
240
+
241
+
242
+ def execute_calls(calls: list[dict], render_context: RenderContext) -> list[dict]:
243
+ """Execute the agent's tool calls in order; every call gets a result, errors included.
244
+
245
+ A repeated read-only call (same tool and arguments, no file changed in between) is not
246
+ re-executed: its result is already in the conversation, so a short pointer is returned."""
247
+ cache = render_context.unit_tests_running_context.tool_result_cache
248
+ results = []
249
+ for call in calls:
250
+ tool = TOOLS.get(call["name"])
251
+ cache_key = json.dumps([call["name"], call.get("args") or {}], sort_keys=True)
252
+ result: dict
253
+ if tool is None:
254
+ result = {"output": f"Error: unknown tool '{call['name']}'."}
255
+ elif call["name"] in READ_ONLY_TOOLS and cache_key in cache:
256
+ result = {
257
+ "output": "Same call as an earlier one and no file has changed since; "
258
+ "its result is unchanged (see the earlier result above)."
259
+ }
260
+ else:
261
+ try:
262
+ output = tool(call.get("args") or {}, render_context)
263
+ result = output if isinstance(output, dict) else {"output": output}
264
+ except Exception as e:
265
+ result = {"output": f"Error: tool '{call['name']}' failed: {type(e).__name__}: {e}"}
266
+ if call["name"] in READ_ONLY_TOOLS and not result["output"].startswith("Error"):
267
+ cache[cache_key] = result["output"]
268
+ console.debug(f"Agent tool {call['name']}({call.get('args')}) -> {result['output'][:200]!r}")
269
+ results.append({"call_id": call["id"], **result})
270
+ return results
@@ -19,6 +19,7 @@ from render_machine.render_types import (
19
19
  FridContext,
20
20
  ScriptExecutionHistory,
21
21
  TestExecutionPhase,
22
+ UnitTestsAgentSession,
22
23
  UnitTestsRunningContext,
23
24
  )
24
25
 
@@ -188,6 +189,14 @@ class RenderContext:
188
189
  def should_run_conformance_tests(self) -> bool:
189
190
  return self.conformance_tests_script is not None
190
191
 
192
+ @property
193
+ def unit_tests_agent_session(self) -> UnitTestsAgentSession:
194
+ """The agent session fixing the unit tests: per unit-test loop, except during the conformance
195
+ phase, where one session spans every loop (see UnitTestsAgentSession)."""
196
+ if self.conformance_tests_running_context is not None:
197
+ return self.conformance_tests_running_context.unit_tests_agent_session
198
+ return self.unit_tests_running_context.agent_session
199
+
191
200
  def start_unittests_processing(self):
192
201
  self.unit_tests_running_context = UnitTestsRunningContext(fix_attempts=0)
193
202
  self.run_state.increment_unittest_batch_id()