codeplain 0.3.4__tar.gz → 0.3.5__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 (238) hide show
  1. codeplain-0.3.5/.claude/settings.local.json +10 -0
  2. {codeplain-0.3.4 → codeplain-0.3.5}/CLAUDE.md +21 -1
  3. {codeplain-0.3.4 → codeplain-0.3.5}/PKG-INFO +7 -7
  4. codeplain-0.3.5/_version.py +1 -0
  5. {codeplain-0.3.4 → codeplain-0.3.5}/file_utils.py +13 -2
  6. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code.py +71 -35
  7. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_exceptions.py +5 -0
  8. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_state.py +1 -0
  9. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_telemetry.py +2 -0
  10. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_utils.py +18 -0
  11. {codeplain-0.3.4 → codeplain-0.3.5}/plain_file.py +53 -1
  12. {codeplain-0.3.4 → codeplain-0.3.5}/plain_spec.py +9 -0
  13. {codeplain-0.3.4 → codeplain-0.3.5}/pyproject.toml +6 -6
  14. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/fix_conformance_test.py +23 -1
  15. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/prepare_testing_environment.py +6 -0
  16. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/refactor_code.py +18 -0
  17. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/render_functional_requirement.py +15 -1
  18. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/render_context.py +0 -55
  19. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/state_machine_config.py +7 -14
  20. {codeplain-0.3.4 → codeplain-0.3.5}/requirements.txt +6 -6
  21. codeplain-0.3.5/tests/data/acceptance_tests_warning/main_requiring_acceptance_tests.plain +16 -0
  22. codeplain-0.3.5/tests/data/acceptance_tests_warning/required_with_acceptance_tests.plain +15 -0
  23. codeplain-0.3.5/tests/data/sample_base64_image.txt +1 -0
  24. codeplain-0.3.5/tests/test_file_utils.py +92 -0
  25. codeplain-0.3.5/tests/test_plain2code.py +94 -0
  26. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_plainfileparser.py +108 -0
  27. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_plainspec.py +36 -0
  28. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_telemetry.py +16 -1
  29. codeplain-0.3.4/_version.py +0 -1
  30. codeplain-0.3.4/standard_template_library/__init__.py +0 -1
  31. codeplain-0.3.4/tests/test_file_utils.py +0 -41
  32. {codeplain-0.3.4 → codeplain-0.3.5}/.claude/settings.json +0 -0
  33. {codeplain-0.3.4 → codeplain-0.3.5}/.flake8 +0 -0
  34. {codeplain-0.3.4 → codeplain-0.3.5}/.github/dependabot.yml +0 -0
  35. {codeplain-0.3.4 → codeplain-0.3.5}/.github/workflows/e2e.yml +0 -0
  36. {codeplain-0.3.4 → codeplain-0.3.5}/.github/workflows/lint-and-test.yml +0 -0
  37. {codeplain-0.3.4 → codeplain-0.3.5}/.github/workflows/nofity-slack-on-main-merge.yml +0 -0
  38. {codeplain-0.3.4 → codeplain-0.3.5}/.github/workflows/publish-install-script.yml +0 -0
  39. {codeplain-0.3.4 → codeplain-0.3.5}/.github/workflows/publish-to-pypi.yml +0 -0
  40. {codeplain-0.3.4 → codeplain-0.3.5}/.gitignore +0 -0
  41. {codeplain-0.3.4 → codeplain-0.3.5}/.vscode/launch.json +0 -0
  42. {codeplain-0.3.4 → codeplain-0.3.5}/.vscode/settings.json +0 -0
  43. {codeplain-0.3.4 → codeplain-0.3.5}/LICENSE +0 -0
  44. {codeplain-0.3.4 → codeplain-0.3.5}/README.md +0 -0
  45. {codeplain-0.3.4 → codeplain-0.3.5}/change_detection.py +0 -0
  46. {codeplain-0.3.4 → codeplain-0.3.5}/cli_output/__init__.py +0 -0
  47. {codeplain-0.3.4 → codeplain-0.3.5}/cli_output/dry_run.py +0 -0
  48. {codeplain-0.3.4 → codeplain-0.3.5}/cli_output/render_summary.py +0 -0
  49. {codeplain-0.3.4 → codeplain-0.3.5}/cli_output/status.py +0 -0
  50. {codeplain-0.3.4 → codeplain-0.3.5}/codeplain_REST_api.py +0 -0
  51. {codeplain-0.3.4 → codeplain-0.3.5}/concept_utils.py +0 -0
  52. {codeplain-0.3.4 → codeplain-0.3.5}/config/__init__.py +0 -0
  53. {codeplain-0.3.4 → codeplain-0.3.5}/config/system_config.yaml +0 -0
  54. {codeplain-0.3.4 → codeplain-0.3.5}/diff_utils.py +0 -0
  55. {codeplain-0.3.4 → codeplain-0.3.5}/docs/generate_cli.py +0 -0
  56. {codeplain-0.3.4 → codeplain-0.3.5}/docs/plain2code_cli.md +0 -0
  57. {codeplain-0.3.4 → codeplain-0.3.5}/docs/starting_a_plain_project_from_scratch.md +0 -0
  58. {codeplain-0.3.4 → codeplain-0.3.5}/event_bus.py +0 -0
  59. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_golang/config.yaml +0 -0
  60. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_golang/harness_tests/hello_world_test.go +0 -0
  61. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_golang/hello_world_golang.plain +0 -0
  62. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_golang/run.sh +0 -0
  63. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_python/config.yaml +0 -0
  64. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py +0 -0
  65. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_python/hello_world_python.plain +0 -0
  66. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_python/run.sh +0 -0
  67. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_react/config.yaml +0 -0
  68. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/e2e/hello_world.cy.ts +0 -0
  69. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/support/e2e.ts +0 -0
  70. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress.config.ts +0 -0
  71. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/package.json +0 -0
  72. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/tsconfig.json +0 -0
  73. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_react/hello_world_react.plain +0 -0
  74. {codeplain-0.3.4 → codeplain-0.3.5}/examples/example_hello_world_react/run.sh +0 -0
  75. {codeplain-0.3.4 → codeplain-0.3.5}/examples/run.sh +0 -0
  76. {codeplain-0.3.4 → codeplain-0.3.5}/git_utils.py +0 -0
  77. {codeplain-0.3.4 → codeplain-0.3.5}/install/bash/examples.sh +0 -0
  78. {codeplain-0.3.4 → codeplain-0.3.5}/install/bash/install.sh +0 -0
  79. {codeplain-0.3.4 → codeplain-0.3.5}/install/bash/walkthrough.sh +0 -0
  80. {codeplain-0.3.4 → codeplain-0.3.5}/install/powershell/examples.ps1 +0 -0
  81. {codeplain-0.3.4 → codeplain-0.3.5}/install/powershell/install.ps1 +0 -0
  82. {codeplain-0.3.4 → codeplain-0.3.5}/install/powershell/walkthrough.ps1 +0 -0
  83. {codeplain-0.3.4 → codeplain-0.3.5}/memory_management.py +0 -0
  84. {codeplain-0.3.4 → codeplain-0.3.5}/module_renderer.py +0 -0
  85. {codeplain-0.3.4 → codeplain-0.3.5}/partial_rendering.py +0 -0
  86. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_arguments.py +0 -0
  87. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_console.py +0 -0
  88. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_events.py +0 -0
  89. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_logger.py +0 -0
  90. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_nodes.py +0 -0
  91. {codeplain-0.3.4 → codeplain-0.3.5}/plain2code_read_config.py +0 -0
  92. {codeplain-0.3.4 → codeplain-0.3.5}/plain_modules.py +0 -0
  93. {codeplain-0.3.4 → codeplain-0.3.5}/pytest.ini +0 -0
  94. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/__init__.py +0 -0
  95. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/analyze_specification_ambiguity.py +0 -0
  96. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/base_action.py +0 -0
  97. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/commit_conformance_tests_changes.py +0 -0
  98. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/commit_implementation_code_changes.py +0 -0
  99. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/create_dist.py +0 -0
  100. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/exit_with_error.py +0 -0
  101. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/finish_functional_requirement.py +0 -0
  102. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/fix_unit_tests.py +0 -0
  103. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/prepare_repositories.py +0 -0
  104. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/render_conformance_tests.py +0 -0
  105. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/run_conformance_tests.py +0 -0
  106. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/run_unit_tests.py +0 -0
  107. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/actions/summarize_conformance_tests.py +0 -0
  108. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/code_renderer.py +0 -0
  109. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/conformance_tests.py +0 -0
  110. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/implementation_code_helpers.py +0 -0
  111. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/render_types.py +0 -0
  112. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/render_utils.py +0 -0
  113. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/states.py +0 -0
  114. {codeplain-0.3.4 → codeplain-0.3.5}/render_machine/triggers.py +0 -0
  115. {codeplain-0.3.4 → codeplain-0.3.5}/resources/codeplain_overview.png +0 -0
  116. {codeplain-0.3.4 → codeplain-0.3.5}/resources/plain_example.png +0 -0
  117. {codeplain-0.3.4 → codeplain-0.3.5}/standard_template_library/golang-console-app-template.plain +0 -0
  118. {codeplain-0.3.4 → codeplain-0.3.5}/standard_template_library/python-console-app-template.plain +0 -0
  119. {codeplain-0.3.4 → codeplain-0.3.5}/standard_template_library/typescript-react-app-boilerplate.plain +0 -0
  120. {codeplain-0.3.4 → codeplain-0.3.5}/standard_template_library/typescript-react-app-template.plain +0 -0
  121. {codeplain-0.3.4 → codeplain-0.3.5}/system_config.py +0 -0
  122. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_conformance_tests_cypress.ps1 +0 -0
  123. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_conformance_tests_cypress.sh +0 -0
  124. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_conformance_tests_golang.ps1 +0 -0
  125. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_conformance_tests_golang.sh +0 -0
  126. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_conformance_tests_python.ps1 +0 -0
  127. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_conformance_tests_python.sh +0 -0
  128. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_unittests_flutter.ps1 +0 -0
  129. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_unittests_golang.ps1 +0 -0
  130. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_unittests_golang.sh +0 -0
  131. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_unittests_python.ps1 +0 -0
  132. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_unittests_python.sh +0 -0
  133. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_unittests_react.ps1 +0 -0
  134. {codeplain-0.3.4 → codeplain-0.3.5}/test_scripts/run_unittests_react.sh +0 -0
  135. {codeplain-0.3.4 → codeplain-0.3.5}/tests/__init__.py +0 -0
  136. {codeplain-0.3.4 → codeplain-0.3.5}/tests/conftest.py +0 -0
  137. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/imports/circular_imports_1.plain +0 -0
  138. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/imports/circular_imports_2.plain +0 -0
  139. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/imports/circular_imports_main.plain +0 -0
  140. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/imports/diamond_import_1.plain +0 -0
  141. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/imports/diamond_import_2.plain +0 -0
  142. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/imports/diamond_import_common.plain +0 -0
  143. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/imports/diamond_imports_main.plain +0 -0
  144. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/imports/non_existent_import.plain +0 -0
  145. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/partial_rendering/pr_code_var.plain +0 -0
  146. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/partial_rendering/pr_implement.plain +0 -0
  147. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/partial_rendering/pr_leaf.plain +0 -0
  148. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/partial_rendering/pr_middle.plain +0 -0
  149. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/partial_rendering/pr_root.plain +0 -0
  150. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/partial_rendering/pr_solo.plain +0 -0
  151. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  152. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  153. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  154. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  155. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  156. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  157. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  158. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
  159. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  160. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  161. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  162. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  163. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  164. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  165. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
  166. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  167. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  168. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
  169. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  170. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  171. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  172. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  173. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  174. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_missing.plain +0 -0
  175. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_missing_example.plain +0 -0
  176. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  177. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  178. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  179. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  180. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  181. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  182. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  183. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  184. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts.plain +0 -0
  185. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  186. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
  187. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  188. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  189. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  190. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  191. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  192. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  193. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/topological_sort.plain +0 -0
  194. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  195. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/circular_requires_main.plain +0 -0
  196. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/circular_requires_sub.plain +0 -0
  197. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/diamond_requires_1.plain +0 -0
  198. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/diamond_requires_2.plain +0 -0
  199. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/diamond_requires_common.plain +0 -0
  200. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/diamond_requires_main.plain +0 -0
  201. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/independent_requires_1.plain +0 -0
  202. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/independent_requires_2.plain +0 -0
  203. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/independent_requires_main.plain +0 -0
  204. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/non_existent_require.plain +0 -0
  205. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/normal_requires_1.plain +0 -0
  206. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/normal_requires_2.plain +0 -0
  207. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/normal_requires_common.plain +0 -0
  208. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/requires/normal_requires_main.plain +0 -0
  209. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/simple.plain +0 -0
  210. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/templates/block_level_include.plain +0 -0
  211. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/templates/code_variables.plain +0 -0
  212. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/templates/header.plain +0 -0
  213. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/templates/implement.plain +0 -0
  214. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/templates/implement_2.plain +0 -0
  215. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/templates/template_include.plain +0 -0
  216. {codeplain-0.3.4 → codeplain-0.3.5}/tests/data/templates/test_hardest_problem.plain +0 -0
  217. {codeplain-0.3.4 → codeplain-0.3.5}/tests/e2e/Dockerfile +0 -0
  218. {codeplain-0.3.4 → codeplain-0.3.5}/tests/e2e/conftest.py +0 -0
  219. {codeplain-0.3.4 → codeplain-0.3.5}/tests/e2e/test_hello_world_python.py +0 -0
  220. {codeplain-0.3.4 → codeplain-0.3.5}/tests/e2e/test_hello_world_python_windows.py +0 -0
  221. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_change_detection.py +0 -0
  222. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_cli_output.py +0 -0
  223. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_git_utils.py +0 -0
  224. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_imports.py +0 -0
  225. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_partial_rendering.py +0 -0
  226. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_plain_modules.py +0 -0
  227. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_plainfile.py +0 -0
  228. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_requires.py +0 -0
  229. {codeplain-0.3.4 → codeplain-0.3.5}/tests/test_resolve_config_file.py +0 -0
  230. {codeplain-0.3.4 → codeplain-0.3.5}/tui/__init__.py +0 -0
  231. {codeplain-0.3.4 → codeplain-0.3.5}/tui/components.py +0 -0
  232. {codeplain-0.3.4 → codeplain-0.3.5}/tui/models.py +0 -0
  233. {codeplain-0.3.4 → codeplain-0.3.5}/tui/plain2code_tui.py +0 -0
  234. {codeplain-0.3.4 → codeplain-0.3.5}/tui/plain_module_render_choice_tui.py +0 -0
  235. {codeplain-0.3.4 → codeplain-0.3.5}/tui/spinner.py +0 -0
  236. {codeplain-0.3.4 → codeplain-0.3.5}/tui/state_handlers.py +0 -0
  237. {codeplain-0.3.4 → codeplain-0.3.5}/tui/styles.css +0 -0
  238. {codeplain-0.3.4 → codeplain-0.3.5}/tui/widget_helpers.py +0 -0
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//Users/nero/DevelCodeplain/**)",
5
+ "Read(//Users/nero/DevelCodeplain/plain2code_rest_api/**)",
6
+ "Bash(/private/tmp/claude-501/-Users-nero-DevelCodeplain-codeplain/8382c3ca-47e9-4519-afd2-160102a63874/scratchpad/tvenv/bin/python /Users/nero/DevelCodeplain/codeplain/plain2code.py hello_world_python.plain --headless)",
7
+ "Bash(echo \"EXIT=$?\")"
8
+ ]
9
+ }
10
+ }
@@ -179,7 +179,7 @@ black . --check && isort . --check-only && flake8 . && mypy . --check-untyped-de
179
179
  - `plain2code_logger.py` - Custom logging with elapsed time timestamps
180
180
  - `plain2code_console.py` - Rich console wrapper with custom styles
181
181
  - `plain2code_state.py` - Runtime state (render ID, counters, timing)
182
- - `standard_template_library/` - Built-in code templates
182
+ - `standard_template_library/` - Built-in `.plain` templates (git subtree from `plainlang-examples`)
183
183
  - `examples/` - Sample `.plain` projects
184
184
 
185
185
  ### Testing Architecture
@@ -223,6 +223,26 @@ Two logging modes:
223
223
  ### Running Plain2Code from Another Directory
224
224
  The tool can be run from any directory by providing an absolute or relative path to the `.plain` file. All paths (build folder, log file, templates) are resolved relative to the `.plain` file's directory, not the current working directory, unless explicitly overridden via CLI arguments.
225
225
 
226
+ ### Standard Template Library
227
+
228
+ `standard_template_library/` is a git subtree sourced from the `plainlang-examples` repository. It is **not** updated automatically — you must pull changes manually when the templates change upstream:
229
+
230
+ ```bash
231
+ git subtree pull --prefix=standard_template_library git@github.com:Codeplain-ai/plainlang-examples.git subtree/standard-template-library --squash
232
+ ```
233
+
234
+ The subtree does not track a branch pointer. The branch (`main` above) is just an argument passed at pull time — git uses the `git-subtree-split` hash embedded in the commit history to determine what's new since the last sync.
235
+
236
+ **Important:** You must pull from the `subtree/standard-template-library` branch, not `main`. This is a split branch containing only the `standard_template_library/` subdirectory contents at root level. Pulling from `main` would bring in the entire `plainlang-examples` repo.
237
+
238
+ If the split branch is outdated, regenerate it first in `plainlang-examples`:
239
+
240
+ ```bash
241
+ # In plainlang-examples/
242
+ git subtree split --prefix=standard_template_library -b subtree/standard-template-library
243
+ git push origin subtree/standard-template-library
244
+ ```
245
+
226
246
  ### Windows Support
227
247
  Windows users must use WSL (Windows Subsystem for Linux). The codebase has some platform-specific script handling (`.ps1` for Windows, `.sh` for Unix).
228
248
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeplain
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -12,20 +12,20 @@ Requires-Dist: gitpython==3.1.50
12
12
  Requires-Dist: mistletoe==1.3.0
13
13
  Requires-Dist: networkx==3.6.1
14
14
  Requires-Dist: python-frontmatter==1.3.0
15
- Requires-Dist: python-liquid2==0.3.0
15
+ Requires-Dist: python-liquid2==0.4.0
16
16
  Requires-Dist: pyyaml==6.0.3
17
- Requires-Dist: requests==2.34.0
17
+ Requires-Dist: requests==2.34.2
18
18
  Requires-Dist: rich==15.0.0
19
- Requires-Dist: sentry-sdk==2.62.0
19
+ Requires-Dist: sentry-sdk==2.63.0
20
20
  Requires-Dist: textual>=7.5.0
21
- Requires-Dist: tiktoken==0.12.0
21
+ Requires-Dist: tiktoken==0.13.0
22
22
  Requires-Dist: transitions==0.9.3
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: black==26.3.1; extra == 'dev'
25
25
  Requires-Dist: flake8==7.3.0; extra == 'dev'
26
26
  Requires-Dist: isort==5.13.2; extra == 'dev'
27
- Requires-Dist: mypy==1.11.2; extra == 'dev'
28
- Requires-Dist: pytest==8.3.5; extra == 'dev'
27
+ Requires-Dist: mypy==2.1.0; extra == 'dev'
28
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
29
29
  Description-Content-Type: text/markdown
30
30
 
31
31
  # Codeplain plain2code renderer
@@ -0,0 +1 @@
1
+ __version__ = "0.3.5"
@@ -8,8 +8,9 @@ from liquid2.exceptions import UndefinedError
8
8
 
9
9
  import plain_spec
10
10
  from plain2code_console import console
11
- from plain2code_exceptions import UnsupportedResourceType
11
+ from plain2code_exceptions import UnsupportedBase64Content, UnsupportedResourceType
12
12
  from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
13
+ from plain2code_utils import find_large_base64_blob
13
14
  from plain_modules import CODEPLAIN_MEMORY_SUBFOLDER, CODEPLAIN_METADATA_FOLDER
14
15
 
15
16
  BINARY_FILE_EXTENSIONS = [".pyc"]
@@ -157,7 +158,7 @@ def store_response_files(target_folder, response_files, existing_files):
157
158
 
158
159
  os.makedirs(os.path.dirname(full_file_name), exist_ok=True)
159
160
 
160
- with open(full_file_name, "w") as f:
161
+ with open(full_file_name, "w", encoding="utf-8") as f:
161
162
  f.write(response_files[file_name])
162
163
 
163
164
  if file_name not in existing_files:
@@ -206,6 +207,16 @@ def load_linked_resources(template_dirs: list[str], resources_list, module_name:
206
207
  Please ensure that the resource exists in one of these locations, or specify the correct --template-dir if using custom templates.
207
208
  """)
208
209
 
210
+ blob = find_large_base64_blob(content)
211
+ if blob is not None:
212
+ raise UnsupportedBase64Content(
213
+ f"Referenced resource '{file_name}' in module '{module_name}' contains a large "
214
+ f"base64-encoded blob ({len(blob)} characters), such as an embedded image. Inline "
215
+ "base64 data is not supported. Remove the data from the resource. "
216
+ "If the data should be used by the end software, "
217
+ "save the data to a separate file and include the file path in the specification without it being a reference file."
218
+ )
219
+
209
220
  linked_resources[file_name] = content
210
221
 
211
222
  return linked_resources
@@ -1,4 +1,3 @@
1
- import importlib.resources
2
1
  import logging
3
2
  import logging.config
4
3
  import os
@@ -37,6 +36,7 @@ from plain2code_exceptions import (
37
36
  PlainSyntaxError,
38
37
  RenderCancelledError,
39
38
  RenderingCreditBalanceTooLow,
39
+ UnsupportedBase64Content,
40
40
  UnsupportedResourceType,
41
41
  )
42
42
  from plain2code_logger import (
@@ -54,7 +54,7 @@ from system_config import system_config
54
54
  from tui.plain2code_tui import Plain2CodeTUI
55
55
  from tui.plain_module_render_choice_tui import PlainModuleRenderChoiceTUI
56
56
 
57
- DEFAULT_TEMPLATE_DIRS = importlib.resources.files("standard_template_library")
57
+ DEFAULT_TEMPLATE_DIRS = "standard_template_library"
58
58
  RENDER_THREAD_SHUTDOWN_TIMEOUT = 0.7
59
59
 
60
60
  # Exceptions that represent expected, user-facing error conditions. They are
@@ -74,6 +74,7 @@ EXPECTED_EXCEPTIONS = (
74
74
  NetworkConnectionError,
75
75
  ModuleDoesNotExistError,
76
76
  UnsupportedResourceType,
77
+ UnsupportedBase64Content,
77
78
  GitNotInstalledError,
78
79
  SystemExit,
79
80
  )
@@ -139,8 +140,8 @@ def setup_logging(
139
140
  return logging.getLevelName(configured_log_level)
140
141
 
141
142
 
142
- def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI):
143
- """Check API connectivity and validate API key and client version."""
143
+ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI) -> Optional[str]:
144
+ """Check API connectivity, validate API key and client version, and return the user email."""
144
145
  response = codeplainAPI.connection_check(system_config.client_version)
145
146
 
146
147
  if not response.get("api_key_valid", False):
@@ -157,33 +158,58 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI):
157
158
  "Please update using: uv tool upgrade codeplain"
158
159
  )
159
160
 
161
+ return response.get("user_email")
160
162
 
161
- def render(args, run_state: RunState, event_bus: EventBus, default_log_level: str = "INFO"): # noqa: C901
162
- template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
163
163
 
164
+ def warn_if_acceptance_tests_without_conformance_script(plain_module, args) -> None:
165
+ """Warn when any loaded module (including required modules) defines acceptance tests
166
+ but no conformance tests script is configured.
167
+
168
+ Acceptance tests are treated as conformance tests, so a conformance tests script is required
169
+ to actually run them. Without it, the acceptance tests cannot be executed.
170
+ """
171
+ if args.conformance_tests_script:
172
+ return
173
+
174
+ module_names_with_acceptance_tests = [
175
+ module.module_name
176
+ for module in plain_module.all_required_modules + [plain_module]
177
+ if plain_spec.has_acceptance_tests(module.plain_source)
178
+ ]
179
+ if not module_names_with_acceptance_tests:
180
+ return
181
+
182
+ module_names = ", ".join(module_names_with_acceptance_tests)
183
+ console.warning(
184
+ f"Acceptance tests were found ({module_names}) but no conformance tests script is configured. "
185
+ "Acceptance tests are treated as conformance tests and require a conformance tests script "
186
+ "(--conformance-tests-script or 'conformance_tests_script' in config) to be executed."
187
+ )
188
+
189
+
190
+ def render( # noqa: C901
191
+ plain_module: plain_modules.PlainModule,
192
+ args,
193
+ run_state: RunState,
194
+ event_bus: EventBus,
195
+ default_log_level: str = "INFO",
196
+ ):
164
197
  # Compute render range from either --render-range or --render-from
165
198
  render_range = None
166
199
  if args.render_range or args.render_from:
167
- # Parse the plain file to get the plain_source for FRID extraction
168
- _, plain_source, _ = plain_file.plain_file_parser(args.filename, template_dirs)
169
- render_range = plain_spec.compute_render_range(args, plain_source)
200
+ render_range = plain_spec.compute_render_range(args, plain_module.plain_source)
170
201
 
171
202
  codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console)
172
203
  assert args.api is not None and args.api != "", "API URL is required"
173
204
  codeplainAPI.api_url = args.api
174
205
 
175
- _check_connection(codeplainAPI)
206
+ run_state.user_email = _check_connection(codeplainAPI)
176
207
 
177
208
  stop_event = threading.Event()
178
209
  enter_pause_event = threading.Event()
179
210
  signal.signal(signal.SIGTERM, lambda _signum, _frame: stop_event.set())
180
211
 
181
- plain_module = plain_modules.PlainModule(
182
- args.filename,
183
- args.build_folder,
184
- args.conformance_tests_folder,
185
- template_dirs,
186
- )
212
+ warn_if_acceptance_tests_without_conformance_script(plain_module, args)
187
213
 
188
214
  render_choice = None
189
215
  if render_range is None:
@@ -308,28 +334,38 @@ def main(): # noqa: C901
308
334
  console.error(f"Error fetching status: {str(e)}")
309
335
  return
310
336
 
311
- # Handle early-exit flags before heavy initialization
312
- if args.dry_run or args.full_plain:
313
- template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
337
+ template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
314
338
 
339
+ # Handle full plain early-exit (raw text dump; does not require a parsed module).
340
+ if args.full_plain:
315
341
  try:
316
- if args.full_plain:
317
- module_name = Path(args.filename).stem
318
- plain_source = plain_file.read_module_plain_source(module_name, template_dirs)
319
- [full_plain_source, _] = file_utils.get_loaded_templates(template_dirs, plain_source)
320
- console.info("Full plain text:\n")
321
- console.info(full_plain_source)
322
- return
323
-
324
- if args.dry_run:
325
- console.info("Printing dry run output...\n")
326
- _, plain_source_tree, _ = plain_file.plain_file_parser(args.filename, template_dirs)
327
- render_range = plain_spec.compute_render_range(args, plain_source_tree)
328
- print_dry_run_output(plain_source_tree, render_range)
329
- return
342
+ module_name = Path(args.filename).stem
343
+ plain_source = plain_file.read_module_plain_source(module_name, template_dirs)
344
+ [full_plain_source, _] = file_utils.get_loaded_templates(template_dirs, plain_source)
345
+ console.info("Full plain text:\n")
346
+ console.info(full_plain_source)
330
347
  except Exception as e:
331
348
  console.error(f"Error: {str(e)}")
332
- return
349
+ return
350
+
351
+ # Parse the plain file (and its required modules) once; reused by dry-run and rendering.
352
+ try:
353
+ plain_module = plain_modules.PlainModule(
354
+ args.filename,
355
+ args.build_folder,
356
+ args.conformance_tests_folder,
357
+ template_dirs,
358
+ )
359
+ except Exception as e:
360
+ console.error(f"Error: {str(e)}")
361
+ return
362
+
363
+ if args.dry_run:
364
+ console.info("Printing dry run output...\n")
365
+ render_range = plain_spec.compute_render_range(args, plain_module.plain_source)
366
+ print_dry_run_output(plain_module.plain_source, render_range)
367
+ warn_if_acceptance_tests_without_conformance_script(plain_module, args)
368
+ return
333
369
 
334
370
  event_bus = EventBus()
335
371
 
@@ -357,7 +393,7 @@ def main(): # noqa: C901
357
393
  raise MissingAPIKey(
358
394
  "Your API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n"
359
395
  )
360
- render(args, run_state, event_bus, default_log_level)
396
+ render(plain_module, args, run_state, event_bus, default_log_level)
361
397
  except BaseException as e:
362
398
  if isinstance(e, KeyboardInterrupt):
363
399
  error_message = "Keyboard interrupt"
@@ -29,6 +29,11 @@ class PlainSyntaxError(Exception):
29
29
  pass
30
30
 
31
31
 
32
+ class UnsupportedBase64Content(Exception):
33
+
34
+ pass
35
+
36
+
32
37
  class InternalClientError(Exception):
33
38
  pass
34
39
 
@@ -27,6 +27,7 @@ class RunState:
27
27
  self.current_module: Optional[str] = None
28
28
  self.current_frid: Optional[str] = None
29
29
  self.current_render_state: Optional[str] = None
30
+ self.user_email: Optional[str] = None
30
31
 
31
32
  def increment_call_count(self):
32
33
  self.call_count += 1
@@ -98,6 +98,8 @@ def capture_crash(exc_info, run_state: Optional[RunState], args) -> bool:
98
98
  scope.set_tag("render_state", run_state.current_render_state)
99
99
  scope.set_tag("current_module", run_state.current_module)
100
100
  scope.set_tag("current_frid", run_state.current_frid)
101
+ if run_state.user_email:
102
+ scope.set_user({"email": run_state.user_email})
101
103
  scope.set_tag("headless", bool(getattr(args, "headless", False)))
102
104
  scope.set_tag("unittests_script_provided", bool(getattr(args, "unittests_script", None)))
103
105
  scope.set_tag("conformance_tests_script_provided", bool(getattr(args, "conformance_tests_script", None)))
@@ -1,3 +1,21 @@
1
+ import re
2
+ from typing import Optional
3
+
4
+ #
5
+ MAX_BASE64_BLOB_LENGTH = 8192
6
+
7
+ # Matches a long contiguous base64 / base64url run, optionally preceded by a data: URI header.
8
+ _BASE64_BLOB_PATTERN = re.compile(
9
+ r"(?:data:[\w.+-]+/[\w.+-]+;base64,)?[A-Za-z0-9+/_-]{%d,}={0,2}" % MAX_BASE64_BLOB_LENGTH
10
+ )
11
+
12
+
13
+ def find_large_base64_blob(text: str) -> Optional[str]:
14
+ """Return the first contiguous base64 blob at or above the threshold, or None."""
15
+ match = _BASE64_BLOB_PATTERN.search(text)
16
+ return match.group(0) if match else None
17
+
18
+
1
19
  def format_duration_hms(total_seconds: int) -> str:
2
20
  """Format a duration in seconds as hours, minutes, and seconds (e.g. ``1h 2m 3.45s``, ``45.67s``)."""
3
21
  if total_seconds < 0:
@@ -17,8 +17,9 @@ from mistletoe.utils import traverse
17
17
  import concept_utils
18
18
  import file_utils
19
19
  import plain_spec
20
- from plain2code_exceptions import ModuleDoesNotExistError, PlainSyntaxError
20
+ from plain2code_exceptions import ModuleDoesNotExistError, PlainSyntaxError, UnsupportedBase64Content
21
21
  from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
22
+ from plain2code_utils import find_large_base64_blob
22
23
 
23
24
  RESOURCE_MARKER = "[resource]"
24
25
 
@@ -203,6 +204,26 @@ def _is_acceptance_test_heading(token) -> tuple[bool, str | None]:
203
204
  return False, None
204
205
 
205
206
 
207
+ def _find_acceptance_test_heading(token):
208
+ """
209
+ Recursively search a token tree for an `acceptance tests` heading.
210
+
211
+ Returns the heading token if one is found anywhere in the subtree, otherwise None.
212
+ Used to detect acceptance tests nested under sections other than functional specs.
213
+ """
214
+ is_acceptance_test_heading, _ = _is_acceptance_test_heading(token)
215
+ if is_acceptance_test_heading:
216
+ return token
217
+
218
+ if getattr(token, "children", None):
219
+ for child in token.children:
220
+ found = _find_acceptance_test_heading(child)
221
+ if found is not None:
222
+ return found
223
+
224
+ return None
225
+
226
+
206
227
  def _process_single_acceptance_test_requirement(functional_requirement: mistletoe.block_token.ListItem):
207
228
  """
208
229
  Process a single functionality to extract acceptance tests.
@@ -473,6 +494,11 @@ def parse_plain_source( # noqa: C901
473
494
 
474
495
  specification_heading = token.children[0].children[0].children[0].content
475
496
 
497
+ if specification_heading == plain_spec.ACCEPTANCE_TEST_HEADING:
498
+ raise PlainSyntaxError(
499
+ f"Plain syntax error: Syntax error at line {token.line_number}: {plain_spec.ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec."
500
+ )
501
+
476
502
  if specification_heading not in plain_spec.ALLOWED_SPECIFICATION_HEADINGS:
477
503
  raise PlainSyntaxError(
478
504
  f"Plain syntax error: Syntax error at line {token.line_number}: Invalid specification heading (`{specification_heading}`). Allowed headings: {', '.join(plain_spec.ALLOWED_SPECIFICATION_HEADINGS)}"
@@ -511,6 +537,22 @@ def parse_plain_source( # noqa: C901
511
537
  f"Plain syntax error: Syntax error at line {token.line_number}: Invalid source structure (`{token_text}`)"
512
538
  )
513
539
 
540
+ # Acceptance tests are only allowed nested under functional specs. Reject them when
541
+ # they are nested under any other section (e.g. definitions, implementation reqs, test reqs).
542
+ for non_functional_heading in (
543
+ plain_spec.DEFINITIONS,
544
+ plain_spec.NON_FUNCTIONAL_REQUIREMENTS,
545
+ plain_spec.TEST_REQUIREMENTS,
546
+ ):
547
+ section = plain_source.get(non_functional_heading)
548
+ if section is None:
549
+ continue
550
+ nested_acceptance_test_heading = _find_acceptance_test_heading(section)
551
+ if nested_acceptance_test_heading is not None:
552
+ raise PlainSyntaxError(
553
+ f"Plain syntax error: Syntax error at line {nested_acceptance_test_heading.line_number}: {plain_spec.ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec."
554
+ )
555
+
514
556
  if plain_source[plain_spec.DEFINITIONS] is not None:
515
557
  with PlainRenderer() as renderer:
516
558
  for token in plain_source[plain_spec.DEFINITIONS].children:
@@ -536,6 +578,16 @@ def read_module_plain_source(module_name: str, template_dirs: list[str]) -> str:
536
578
  plain_source_text = file_utils.open_from(template_dirs, module_name + PLAIN_SOURCE_FILE_EXTENSION)
537
579
  if plain_source_text is None:
538
580
  raise ModuleDoesNotExistError(f"Module does not exist ({module_name}).")
581
+
582
+ blob = find_large_base64_blob(plain_source_text)
583
+ if blob is not None:
584
+ raise UnsupportedBase64Content(
585
+ f"Module '{module_name}' contains a base64-encoded blob ({len(blob)} characters) "
586
+ "inlined in the specification. This is not supported."
587
+ "Remove the base64 data from the .plain file or if necessary,"
588
+ "include the binary file path in the specification."
589
+ )
590
+
539
591
  return plain_source_text
540
592
 
541
593
 
@@ -25,6 +25,15 @@ ALLOWED_SPECIFICATION_HEADINGS = [
25
25
  ALLOWED_IMPORT_SPECIFICATION_HEADINGS = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS]
26
26
 
27
27
 
28
+ def has_acceptance_tests(plain_source_tree) -> bool:
29
+ """Return True if any functional requirement in the plain source defines acceptance tests."""
30
+ functional_requirements = plain_source_tree.get(FUNCTIONAL_REQUIREMENTS) or []
31
+ for functional_requirement in functional_requirements:
32
+ if functional_requirement.get(ACCEPTANCE_TESTS):
33
+ return True
34
+ return False
35
+
36
+
28
37
  def collect_specification_linked_resources(specification, specification_heading, linked_resources_list):
29
38
  linked_resources = []
30
39
  if "linked_resources" in specification:
@@ -15,10 +15,10 @@ classifiers = [
15
15
  "Topic :: Software Development :: Code Generators",
16
16
  ]
17
17
  dependencies = [
18
- "python-liquid2==0.3.0",
18
+ "python-liquid2==0.4.0",
19
19
  "mistletoe==1.3.0",
20
- "requests==2.34.0",
21
- "tiktoken==0.12.0",
20
+ "requests==2.34.2",
21
+ "tiktoken==0.13.0",
22
22
  "PyYAML==6.0.3",
23
23
  "gitpython==3.1.50",
24
24
  "transitions==0.9.3",
@@ -26,16 +26,16 @@ dependencies = [
26
26
  "rich==15.0.0",
27
27
  "python-frontmatter==1.3.0",
28
28
  "networkx==3.6.1",
29
- "sentry-sdk==2.62.0",
29
+ "sentry-sdk==2.63.0",
30
30
  ]
31
31
 
32
32
  [project.optional-dependencies]
33
33
  dev = [
34
- "pytest==8.3.5",
34
+ "pytest==9.1.1",
35
35
  "flake8==7.3.0",
36
36
  "black==26.3.1",
37
37
  "isort==5.13.2",
38
- "mypy==1.11.2",
38
+ "mypy==2.1.0",
39
39
  ]
40
40
 
41
41
  [project.scripts]
@@ -9,12 +9,17 @@ from plain2code_exceptions import InternalClientError
9
9
  from render_machine.actions.base_action import BaseAction
10
10
  from render_machine.implementation_code_helpers import ImplementationCodeHelpers
11
11
  from render_machine.render_context import RenderContext
12
- from render_machine.render_types import TestExecutionPhase
12
+ from render_machine.render_types import RenderError, TestExecutionPhase
13
+
14
+ MAX_CONFORMANCE_TEST_FIX_ATTEMPTS = 20
15
+ MAX_CONFORMANCE_TEST_RERENDER_ATTEMPTS = 1
13
16
 
14
17
 
15
18
  class FixConformanceTest(BaseAction):
16
19
  IMPLEMENTATION_CODE_NOT_UPDATED = "implementation_code_not_updated"
17
20
  IMPLEMENTATION_CODE_UPDATED = "implementation_code_updated"
21
+ LIMIT_EXCEEDED_OUTCOME = "conformance_test_fix_limit_exceeded"
22
+ REGENERATE_CONFORMANCE_TESTS_OUTCOME = "regenerate_conformance_tests"
18
23
 
19
24
  ISSUE_REASON_CODE_CONFORMANCE_TESTS = 0
20
25
  ISSUE_REASON_CODE_IMPLEMENTATION_CODE = 1
@@ -22,6 +27,23 @@ class FixConformanceTest(BaseAction):
22
27
  ISSUE_REASON_CODE_CONFLICTING_ACCEPTANCE_TESTS = 3
23
28
 
24
29
  def execute(self, render_context: RenderContext, previous_action_payload: Any | None):
30
+ ctx = render_context.conformance_tests_running_context
31
+ ctx.fix_attempts += 1
32
+
33
+ if ctx.fix_attempts >= MAX_CONFORMANCE_TEST_FIX_ATTEMPTS:
34
+ if ctx.conformance_tests_render_attempts >= MAX_CONFORMANCE_TEST_RERENDER_ATTEMPTS:
35
+ error_msg = f"The renderer was unable to produce an implementation that passes conformance tests for functionality '{render_context.frid_context.frid}' after many attempts. Please review and rewrite the specification. (Render ID: {render_context.run_state.render_id})"
36
+ render_context.last_error_message = error_msg
37
+ return (
38
+ self.LIMIT_EXCEEDED_OUTCOME,
39
+ RenderError.encode(message=error_msg).to_payload(),
40
+ )
41
+ else:
42
+ ctx.regenerating_conformance_tests = True
43
+ return self.REGENERATE_CONFORMANCE_TESTS_OUTCOME, None
44
+
45
+ console.info(f"Running conformance tests attempt {ctx.fix_attempts + 1}.")
46
+
25
47
  console.info(
26
48
  f"Fixing conformance test for functionality {render_context.conformance_tests_running_context.current_testing_frid} in module {render_context.conformance_tests_running_context.current_testing_module_name}."
27
49
  )
@@ -12,6 +12,12 @@ class PrepareTestingEnvironment(BaseAction):
12
12
  FAILED_OUTCOME = "testing_environment_preparation_failed"
13
13
 
14
14
  def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
15
+ if (
16
+ render_context.prepare_environment_script is None
17
+ or not render_context.conformance_tests_running_context.should_prepare_testing_environment
18
+ ):
19
+ return self.SUCCESSFUL_OUTCOME, None
20
+
15
21
  console.info(
16
22
  f"Running testing environment preparation script {render_context.prepare_environment_script} for build folder {render_context.build_folder}."
17
23
  )
@@ -5,13 +5,31 @@ from plain2code_console import console
5
5
  from render_machine.actions.base_action import BaseAction
6
6
  from render_machine.implementation_code_helpers import ImplementationCodeHelpers
7
7
  from render_machine.render_context import RenderContext
8
+ from render_machine.render_types import RenderError
9
+
10
+ MAX_REFACTORING_ITERATIONS = 5
8
11
 
9
12
 
10
13
  class RefactorCode(BaseAction):
11
14
  SUCCESSFUL_OUTCOME = "refactoring_successful"
12
15
  NO_FILES_REFACTORED_OUTCOME = "no_files_refactored"
16
+ ITERATION_LIMIT_EXCEEDED_OUTCOME = "refactoring_iteration_limit_exceeded"
13
17
 
14
18
  def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
19
+ if render_context.frid_context.refactoring_iteration == 0:
20
+ console.info("Refactoring the generated code...")
21
+
22
+ render_context.frid_context.refactoring_iteration += 1
23
+
24
+ if render_context.frid_context.refactoring_iteration >= MAX_REFACTORING_ITERATIONS:
25
+ error_message = "Refactoring iterations limit of {MAX_REFACTORING_ITERATIONS} reached for functionality {render_context.frid_context.frid}."
26
+ render_context.last_error_message = error_message
27
+
28
+ return (
29
+ self.ITERATION_LIMIT_EXCEEDED_OUTCOME,
30
+ RenderError.encode(message=error_message).to_payload(),
31
+ )
32
+
15
33
  existing_files, existing_files_content = ImplementationCodeHelpers.fetch_existing_files(
16
34
  render_context.build_folder
17
35
  )
@@ -16,8 +16,22 @@ MAX_CODE_GENERATION_RETRIES = 2
16
16
  class RenderFunctionalRequirement(BaseAction):
17
17
  SUCCESSFUL_OUTCOME = "code_and_unit_tests_generated"
18
18
  FUNCTIONAL_REQUIREMENT_TOO_COMPLEX_OUTCOME = "functional_requirement_too_complex"
19
+ ITERATION_LIMIT_EXCEEDED_OUTCOME = "frid_iteration_limit_exceeded"
19
20
 
20
21
  def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
22
+ if render_context.frid_context.functional_requirement_render_attempts >= MAX_CODE_GENERATION_RETRIES:
23
+ error_msg = f"Unittests could not be fixed after rendering the functionality {render_context.frid_context.frid} for the {MAX_CODE_GENERATION_RETRIES} times."
24
+ render_context.last_error_message = error_msg
25
+ return self.ITERATION_LIMIT_EXCEEDED_OUTCOME, None
26
+
27
+ render_context.frid_context.functional_requirement_render_attempts += 1
28
+
29
+ if render_context.frid_context.functional_requirement_render_attempts > 1:
30
+ console.info(
31
+ f"Unittests could not be fixed after rendering the functionality. "
32
+ f"Restarting rendering functionality {render_context.frid_context.frid} from scratch."
33
+ )
34
+
21
35
  render_utils.revert_changes_for_frid(render_context)
22
36
  existing_files, existing_files_content = ImplementationCodeHelpers.fetch_existing_files(
23
37
  render_context.build_folder
@@ -48,7 +62,7 @@ class RenderFunctionalRequirement(BaseAction):
48
62
  render_context.run_state,
49
63
  )
50
64
  except FunctionalRequirementTooComplex as e:
51
- 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)})."
65
+ 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."
52
66
  if e.proposed_breakdown:
53
67
  error_message += "\nProposed breakdown:"
54
68
  for _, part in e.proposed_breakdown["functional_requirements"].items():