codeplain 0.3.3__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 (240) hide show
  1. codeplain-0.3.5/.claude/settings.local.json +10 -0
  2. {codeplain-0.3.3 → codeplain-0.3.5}/CLAUDE.md +21 -1
  3. {codeplain-0.3.3 → codeplain-0.3.5}/PKG-INFO +7 -7
  4. codeplain-0.3.5/_version.py +1 -0
  5. codeplain-0.3.5/change_detection.py +190 -0
  6. {codeplain-0.3.3 → codeplain-0.3.5}/file_utils.py +13 -2
  7. codeplain-0.3.5/partial_rendering.py +297 -0
  8. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code.py +71 -35
  9. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_exceptions.py +5 -0
  10. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_state.py +1 -0
  11. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_telemetry.py +2 -0
  12. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_utils.py +18 -0
  13. {codeplain-0.3.3 → codeplain-0.3.5}/plain_file.py +61 -1
  14. {codeplain-0.3.3 → codeplain-0.3.5}/plain_modules.py +36 -2
  15. {codeplain-0.3.3 → codeplain-0.3.5}/plain_spec.py +9 -0
  16. {codeplain-0.3.3 → codeplain-0.3.5}/pyproject.toml +6 -6
  17. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/finish_functional_requirement.py +2 -0
  18. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/fix_conformance_test.py +23 -1
  19. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/prepare_testing_environment.py +6 -0
  20. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/refactor_code.py +18 -0
  21. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/render_functional_requirement.py +15 -1
  22. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/render_context.py +0 -55
  23. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/state_machine_config.py +7 -14
  24. {codeplain-0.3.3 → codeplain-0.3.5}/requirements.txt +6 -6
  25. codeplain-0.3.5/tests/data/acceptance_tests_warning/main_requiring_acceptance_tests.plain +16 -0
  26. codeplain-0.3.5/tests/data/acceptance_tests_warning/required_with_acceptance_tests.plain +15 -0
  27. codeplain-0.3.5/tests/data/partial_rendering/pr_code_var.plain +9 -0
  28. codeplain-0.3.5/tests/data/partial_rendering/pr_implement.plain +2 -0
  29. codeplain-0.3.5/tests/data/sample_base64_image.txt +1 -0
  30. codeplain-0.3.5/tests/test_change_detection.py +394 -0
  31. codeplain-0.3.5/tests/test_file_utils.py +92 -0
  32. codeplain-0.3.5/tests/test_partial_rendering.py +571 -0
  33. codeplain-0.3.5/tests/test_plain2code.py +94 -0
  34. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_plain_modules.py +49 -0
  35. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_plainfileparser.py +108 -0
  36. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_plainspec.py +36 -0
  37. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_telemetry.py +16 -1
  38. {codeplain-0.3.3 → codeplain-0.3.5}/tui/plain_module_render_choice_tui.py +33 -4
  39. codeplain-0.3.3/_version.py +0 -1
  40. codeplain-0.3.3/partial_rendering.py +0 -228
  41. codeplain-0.3.3/standard_template_library/__init__.py +0 -1
  42. codeplain-0.3.3/tests/test_file_utils.py +0 -41
  43. codeplain-0.3.3/tests/test_partial_rendering.py +0 -316
  44. {codeplain-0.3.3 → codeplain-0.3.5}/.claude/settings.json +0 -0
  45. {codeplain-0.3.3 → codeplain-0.3.5}/.flake8 +0 -0
  46. {codeplain-0.3.3 → codeplain-0.3.5}/.github/dependabot.yml +0 -0
  47. {codeplain-0.3.3 → codeplain-0.3.5}/.github/workflows/e2e.yml +0 -0
  48. {codeplain-0.3.3 → codeplain-0.3.5}/.github/workflows/lint-and-test.yml +0 -0
  49. {codeplain-0.3.3 → codeplain-0.3.5}/.github/workflows/nofity-slack-on-main-merge.yml +0 -0
  50. {codeplain-0.3.3 → codeplain-0.3.5}/.github/workflows/publish-install-script.yml +0 -0
  51. {codeplain-0.3.3 → codeplain-0.3.5}/.github/workflows/publish-to-pypi.yml +0 -0
  52. {codeplain-0.3.3 → codeplain-0.3.5}/.gitignore +0 -0
  53. {codeplain-0.3.3 → codeplain-0.3.5}/.vscode/launch.json +0 -0
  54. {codeplain-0.3.3 → codeplain-0.3.5}/.vscode/settings.json +0 -0
  55. {codeplain-0.3.3 → codeplain-0.3.5}/LICENSE +0 -0
  56. {codeplain-0.3.3 → codeplain-0.3.5}/README.md +0 -0
  57. {codeplain-0.3.3 → codeplain-0.3.5}/cli_output/__init__.py +0 -0
  58. {codeplain-0.3.3 → codeplain-0.3.5}/cli_output/dry_run.py +0 -0
  59. {codeplain-0.3.3 → codeplain-0.3.5}/cli_output/render_summary.py +0 -0
  60. {codeplain-0.3.3 → codeplain-0.3.5}/cli_output/status.py +0 -0
  61. {codeplain-0.3.3 → codeplain-0.3.5}/codeplain_REST_api.py +0 -0
  62. {codeplain-0.3.3 → codeplain-0.3.5}/concept_utils.py +0 -0
  63. {codeplain-0.3.3 → codeplain-0.3.5}/config/__init__.py +0 -0
  64. {codeplain-0.3.3 → codeplain-0.3.5}/config/system_config.yaml +0 -0
  65. {codeplain-0.3.3 → codeplain-0.3.5}/diff_utils.py +0 -0
  66. {codeplain-0.3.3 → codeplain-0.3.5}/docs/generate_cli.py +0 -0
  67. {codeplain-0.3.3 → codeplain-0.3.5}/docs/plain2code_cli.md +0 -0
  68. {codeplain-0.3.3 → codeplain-0.3.5}/docs/starting_a_plain_project_from_scratch.md +0 -0
  69. {codeplain-0.3.3 → codeplain-0.3.5}/event_bus.py +0 -0
  70. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_golang/config.yaml +0 -0
  71. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_golang/harness_tests/hello_world_test.go +0 -0
  72. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_golang/hello_world_golang.plain +0 -0
  73. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_golang/run.sh +0 -0
  74. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_python/config.yaml +0 -0
  75. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py +0 -0
  76. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_python/hello_world_python.plain +0 -0
  77. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_python/run.sh +0 -0
  78. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_react/config.yaml +0 -0
  79. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/e2e/hello_world.cy.ts +0 -0
  80. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/support/e2e.ts +0 -0
  81. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress.config.ts +0 -0
  82. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/package.json +0 -0
  83. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_react/harness_tests/hello_world_display/tsconfig.json +0 -0
  84. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_react/hello_world_react.plain +0 -0
  85. {codeplain-0.3.3 → codeplain-0.3.5}/examples/example_hello_world_react/run.sh +0 -0
  86. {codeplain-0.3.3 → codeplain-0.3.5}/examples/run.sh +0 -0
  87. {codeplain-0.3.3 → codeplain-0.3.5}/git_utils.py +0 -0
  88. {codeplain-0.3.3 → codeplain-0.3.5}/install/bash/examples.sh +0 -0
  89. {codeplain-0.3.3 → codeplain-0.3.5}/install/bash/install.sh +0 -0
  90. {codeplain-0.3.3 → codeplain-0.3.5}/install/bash/walkthrough.sh +0 -0
  91. {codeplain-0.3.3 → codeplain-0.3.5}/install/powershell/examples.ps1 +0 -0
  92. {codeplain-0.3.3 → codeplain-0.3.5}/install/powershell/install.ps1 +0 -0
  93. {codeplain-0.3.3 → codeplain-0.3.5}/install/powershell/walkthrough.ps1 +0 -0
  94. {codeplain-0.3.3 → codeplain-0.3.5}/memory_management.py +0 -0
  95. {codeplain-0.3.3 → codeplain-0.3.5}/module_renderer.py +0 -0
  96. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_arguments.py +0 -0
  97. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_console.py +0 -0
  98. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_events.py +0 -0
  99. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_logger.py +0 -0
  100. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_nodes.py +0 -0
  101. {codeplain-0.3.3 → codeplain-0.3.5}/plain2code_read_config.py +0 -0
  102. {codeplain-0.3.3 → codeplain-0.3.5}/pytest.ini +0 -0
  103. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/__init__.py +0 -0
  104. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/analyze_specification_ambiguity.py +0 -0
  105. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/base_action.py +0 -0
  106. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/commit_conformance_tests_changes.py +0 -0
  107. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/commit_implementation_code_changes.py +0 -0
  108. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/create_dist.py +0 -0
  109. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/exit_with_error.py +0 -0
  110. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/fix_unit_tests.py +0 -0
  111. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/prepare_repositories.py +0 -0
  112. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/render_conformance_tests.py +0 -0
  113. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/run_conformance_tests.py +0 -0
  114. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/run_unit_tests.py +0 -0
  115. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/actions/summarize_conformance_tests.py +0 -0
  116. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/code_renderer.py +0 -0
  117. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/conformance_tests.py +0 -0
  118. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/implementation_code_helpers.py +0 -0
  119. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/render_types.py +0 -0
  120. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/render_utils.py +0 -0
  121. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/states.py +0 -0
  122. {codeplain-0.3.3 → codeplain-0.3.5}/render_machine/triggers.py +0 -0
  123. {codeplain-0.3.3 → codeplain-0.3.5}/resources/codeplain_overview.png +0 -0
  124. {codeplain-0.3.3 → codeplain-0.3.5}/resources/plain_example.png +0 -0
  125. {codeplain-0.3.3 → codeplain-0.3.5}/standard_template_library/golang-console-app-template.plain +0 -0
  126. {codeplain-0.3.3 → codeplain-0.3.5}/standard_template_library/python-console-app-template.plain +0 -0
  127. {codeplain-0.3.3 → codeplain-0.3.5}/standard_template_library/typescript-react-app-boilerplate.plain +0 -0
  128. {codeplain-0.3.3 → codeplain-0.3.5}/standard_template_library/typescript-react-app-template.plain +0 -0
  129. {codeplain-0.3.3 → codeplain-0.3.5}/system_config.py +0 -0
  130. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_conformance_tests_cypress.ps1 +0 -0
  131. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_conformance_tests_cypress.sh +0 -0
  132. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_conformance_tests_golang.ps1 +0 -0
  133. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_conformance_tests_golang.sh +0 -0
  134. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_conformance_tests_python.ps1 +0 -0
  135. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_conformance_tests_python.sh +0 -0
  136. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_unittests_flutter.ps1 +0 -0
  137. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_unittests_golang.ps1 +0 -0
  138. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_unittests_golang.sh +0 -0
  139. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_unittests_python.ps1 +0 -0
  140. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_unittests_python.sh +0 -0
  141. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_unittests_react.ps1 +0 -0
  142. {codeplain-0.3.3 → codeplain-0.3.5}/test_scripts/run_unittests_react.sh +0 -0
  143. {codeplain-0.3.3 → codeplain-0.3.5}/tests/__init__.py +0 -0
  144. {codeplain-0.3.3 → codeplain-0.3.5}/tests/conftest.py +0 -0
  145. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/imports/circular_imports_1.plain +0 -0
  146. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/imports/circular_imports_2.plain +0 -0
  147. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/imports/circular_imports_main.plain +0 -0
  148. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/imports/diamond_import_1.plain +0 -0
  149. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/imports/diamond_import_2.plain +0 -0
  150. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/imports/diamond_import_common.plain +0 -0
  151. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/imports/diamond_imports_main.plain +0 -0
  152. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/imports/non_existent_import.plain +0 -0
  153. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/partial_rendering/pr_leaf.plain +0 -0
  154. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/partial_rendering/pr_middle.plain +0 -0
  155. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/partial_rendering/pr_root.plain +0 -0
  156. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/partial_rendering/pr_solo.plain +0 -0
  157. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  158. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  159. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  160. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  161. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  162. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  163. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  164. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
  165. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  166. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  167. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  168. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  169. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  170. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  171. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
  172. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  173. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  174. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
  175. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  176. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  177. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  178. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  179. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  180. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_missing.plain +0 -0
  181. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_missing_example.plain +0 -0
  182. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  183. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  184. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  185. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  186. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  187. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  188. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  189. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  190. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts.plain +0 -0
  191. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  192. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
  193. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  194. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  195. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  196. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  197. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  198. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  199. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/topological_sort.plain +0 -0
  200. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  201. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/circular_requires_main.plain +0 -0
  202. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/circular_requires_sub.plain +0 -0
  203. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/diamond_requires_1.plain +0 -0
  204. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/diamond_requires_2.plain +0 -0
  205. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/diamond_requires_common.plain +0 -0
  206. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/diamond_requires_main.plain +0 -0
  207. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/independent_requires_1.plain +0 -0
  208. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/independent_requires_2.plain +0 -0
  209. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/independent_requires_main.plain +0 -0
  210. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/non_existent_require.plain +0 -0
  211. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/normal_requires_1.plain +0 -0
  212. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/normal_requires_2.plain +0 -0
  213. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/normal_requires_common.plain +0 -0
  214. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/requires/normal_requires_main.plain +0 -0
  215. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/simple.plain +0 -0
  216. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/templates/block_level_include.plain +0 -0
  217. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/templates/code_variables.plain +0 -0
  218. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/templates/header.plain +0 -0
  219. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/templates/implement.plain +0 -0
  220. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/templates/implement_2.plain +0 -0
  221. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/templates/template_include.plain +0 -0
  222. {codeplain-0.3.3 → codeplain-0.3.5}/tests/data/templates/test_hardest_problem.plain +0 -0
  223. {codeplain-0.3.3 → codeplain-0.3.5}/tests/e2e/Dockerfile +0 -0
  224. {codeplain-0.3.3 → codeplain-0.3.5}/tests/e2e/conftest.py +0 -0
  225. {codeplain-0.3.3 → codeplain-0.3.5}/tests/e2e/test_hello_world_python.py +0 -0
  226. {codeplain-0.3.3 → codeplain-0.3.5}/tests/e2e/test_hello_world_python_windows.py +0 -0
  227. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_cli_output.py +0 -0
  228. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_git_utils.py +0 -0
  229. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_imports.py +0 -0
  230. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_plainfile.py +0 -0
  231. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_requires.py +0 -0
  232. {codeplain-0.3.3 → codeplain-0.3.5}/tests/test_resolve_config_file.py +0 -0
  233. {codeplain-0.3.3 → codeplain-0.3.5}/tui/__init__.py +0 -0
  234. {codeplain-0.3.3 → codeplain-0.3.5}/tui/components.py +0 -0
  235. {codeplain-0.3.3 → codeplain-0.3.5}/tui/models.py +0 -0
  236. {codeplain-0.3.3 → codeplain-0.3.5}/tui/plain2code_tui.py +0 -0
  237. {codeplain-0.3.3 → codeplain-0.3.5}/tui/spinner.py +0 -0
  238. {codeplain-0.3.3 → codeplain-0.3.5}/tui/state_handlers.py +0 -0
  239. {codeplain-0.3.3 → codeplain-0.3.5}/tui/styles.css +0 -0
  240. {codeplain-0.3.3 → 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.3
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"
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Literal
5
+
6
+ if TYPE_CHECKING:
7
+ from plain_modules import PlainModule
8
+
9
+ MODULE_FUNCTIONALITIES_KEY = "functionalities"
10
+ NON_FUNCTIONAL_SOURCE_HASH_KEY = "non_functional_source_hash"
11
+
12
+
13
+ @dataclass
14
+ class FunctionalityChange:
15
+ module: str
16
+ frid: str
17
+ change_type: Literal["added", "removed", "edited", "moved"]
18
+ detail: str | None = None
19
+
20
+
21
+ @dataclass
22
+ class PartialRenderStart:
23
+ module: "PlainModule"
24
+ frid: str
25
+
26
+
27
+ def determine_partial_render_start(plain_module: "PlainModule") -> PartialRenderStart | None:
28
+ """Determine where to start partial rendering based on spec changes.
29
+
30
+ Returns None (only full render is safe) if non-FR sections changed
31
+ (e.g. definitions, implementation reqs) since previously-rendered FRs
32
+ were generated without that context, if no changes are found, or if
33
+ all changes are trailing removals that don't require rendering.
34
+ """
35
+ all_modules = plain_module.all_required_modules + [plain_module]
36
+
37
+ for module in all_modules:
38
+ if _non_functional_content_changed(module):
39
+ return None
40
+
41
+ changes = _detect_module_changes(module)
42
+ if not changes:
43
+ continue
44
+
45
+ current_fr_count = len(module._get_module_functional_requirements())
46
+ earliest_frid = _get_earliest_affected_frid(changes, current_fr_count)
47
+ if earliest_frid is None:
48
+ continue
49
+ return PartialRenderStart(module=module, frid=earliest_frid)
50
+
51
+ return None
52
+
53
+
54
+ def _non_functional_content_changed(module: "PlainModule") -> bool:
55
+ """Check whether anything outside functional specs changed since last render.
56
+
57
+ A missing stored hash (older builds) is treated as changed — partial rendering
58
+ is unsafe without a known baseline.
59
+ """
60
+ metadata = module.load_module_metadata()
61
+ if not metadata:
62
+ return False
63
+ stored_hash = metadata.get(NON_FUNCTIONAL_SOURCE_HASH_KEY)
64
+ if stored_hash is None:
65
+ return True
66
+ return stored_hash != module.get_module_non_functional_source_hash()
67
+
68
+
69
+ def _get_earliest_affected_frid(changes: list[FunctionalityChange], current_fr_count: int) -> str | None:
70
+ """Earliest FRID (in current spec numbering) that must be re-rendered.
71
+
72
+ Returns the minimum position across all changes:
73
+ - added / edited: the FRID itself.
74
+ - removed: the position the removal opened up (now occupied by the next FR).
75
+ - moved: the FR's old position.
76
+
77
+ Correctness does not rely on any single change type's position being individually
78
+ "the" earliest (in particular, a move's old position is *not* always its earliest
79
+ touched position once moves mix with adds/removes). The guarantee is structural:
80
+ the first index at which the new spec diverges from the old is always emitted as
81
+ some change anchored at that index — a move with that old position, or an
82
+ edit/removal/addition there — so the minimum over all change FRIDs lands at or
83
+ before the true first divergence. Rendering runs from this FRID to the end of the
84
+ module, so an at-or-before start point is always safe (it can re-render unchanged
85
+ trailing FRs, but never skips a changed one).
86
+
87
+ Returns None when every change is a removal beyond the current spec length
88
+ (only trailing FRs were removed, so nothing needs rendering).
89
+ """
90
+ earliest = None
91
+ for change in changes:
92
+ frid_int = int(change.frid)
93
+ if change.change_type == "removed" and frid_int > current_fr_count:
94
+ continue
95
+ if earliest is None or frid_int < earliest:
96
+ earliest = frid_int
97
+ if earliest is None:
98
+ return None
99
+ return str(earliest)
100
+
101
+
102
+ def _detect_module_changes(module: "PlainModule") -> list[FunctionalityChange]:
103
+ metadata = module.load_module_metadata()
104
+ old_frs: list[str] = metadata.get(MODULE_FUNCTIONALITIES_KEY, []) if metadata else []
105
+ new_frs: list[str] = module._get_module_functional_requirements()
106
+
107
+ if old_frs == new_frs:
108
+ return []
109
+
110
+ moves, edits, removed, added = _classify_changes(old_frs, new_frs)
111
+
112
+ changes: list[FunctionalityChange] = []
113
+ name = module.module_name
114
+
115
+ for old_idx, new_idx in moves:
116
+ old_frid = _frid_from_index(old_idx)
117
+ new_frid = _frid_from_index(new_idx)
118
+ changes.append(FunctionalityChange(module=name, frid=old_frid, change_type="moved", detail=new_frid))
119
+
120
+ for idx in edits:
121
+ changes.append(FunctionalityChange(module=name, frid=_frid_from_index(idx), change_type="edited"))
122
+
123
+ for idx in removed:
124
+ changes.append(FunctionalityChange(module=name, frid=_frid_from_index(idx), change_type="removed"))
125
+
126
+ for idx in added:
127
+ changes.append(FunctionalityChange(module=name, frid=_frid_from_index(idx), change_type="added"))
128
+
129
+ return changes
130
+
131
+
132
+ def _classify_changes(
133
+ old_frs: list[str], new_frs: list[str]
134
+ ) -> tuple[list[tuple[int, int]], list[int], list[int], list[int]]:
135
+ matched_old: set[int] = set()
136
+ matched_new: set[int] = set()
137
+
138
+ for i in range(min(len(old_frs), len(new_frs))):
139
+ if old_frs[i] == new_frs[i]:
140
+ matched_old.add(i)
141
+ matched_new.add(i)
142
+
143
+ content_matches: list[tuple[int, int]] = []
144
+ for old_idx in range(len(old_frs)):
145
+ if old_idx in matched_old:
146
+ continue
147
+ for new_idx in range(len(new_frs)):
148
+ if new_idx in matched_new:
149
+ continue
150
+ if old_frs[old_idx] == new_frs[new_idx]:
151
+ content_matches.append((old_idx, new_idx))
152
+ matched_old.add(old_idx)
153
+ matched_new.add(new_idx)
154
+ break
155
+
156
+ moves: list[tuple[int, int]] = []
157
+ if content_matches and _has_relative_order_change(content_matches):
158
+ moves = content_matches
159
+
160
+ edits: list[int] = []
161
+ for i in range(min(len(old_frs), len(new_frs))):
162
+ if i not in matched_old and i not in matched_new:
163
+ edits.append(i)
164
+ matched_old.add(i)
165
+ matched_new.add(i)
166
+
167
+ removed = [i for i in range(len(old_frs)) if i not in matched_old]
168
+ added = [i for i in range(len(new_frs)) if i not in matched_new]
169
+
170
+ return moves, edits, removed, added
171
+
172
+
173
+ def _has_relative_order_change(matches: list[tuple[int, int]]) -> bool:
174
+ """Check if content matches represent a true reorder (relative order changed).
175
+
176
+ If all matches preserve relative order (sorted by old_idx gives same ordering
177
+ as sorted by new_idx), it's just a positional shift from insertions/removals.
178
+ """
179
+ if len(matches) <= 1:
180
+ return False
181
+ sorted_by_old = sorted(matches, key=lambda m: m[0])
182
+ new_indices = [m[1] for m in sorted_by_old]
183
+ for i in range(len(new_indices) - 1):
184
+ if new_indices[i] > new_indices[i + 1]:
185
+ return True
186
+ return False
187
+
188
+
189
+ def _frid_from_index(index: int) -> str:
190
+ return str(index + 1)
@@ -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
@@ -0,0 +1,297 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+ import plain_spec
5
+ from change_detection import PartialRenderStart, determine_partial_render_start
6
+ from plain2code_exceptions import ModuleDoesNotExistError
7
+ from plain_modules import PlainModule
8
+
9
+
10
+ @dataclass
11
+ class PlainModuleRenderState:
12
+ last_render_module: PlainModule
13
+ last_render_frid: str | None
14
+ change: PlainModule | None = None
15
+ change_type: Literal["spec_change", "code_change"] | None = None
16
+
17
+
18
+ @dataclass
19
+ class RenderChoice:
20
+ module: PlainModule | None = None
21
+ render_range: list[str] | None = None
22
+ wipe_later_modules: bool = False
23
+ is_destructive: bool = False
24
+ choice_type: str | None = None
25
+
26
+
27
+ def spec_change(plain_module: PlainModule) -> PlainModule | None:
28
+ all_modules = plain_module.all_required_modules + [plain_module]
29
+ for _module in all_modules:
30
+ module_metadata = _module.load_module_metadata()
31
+ if (
32
+ module_metadata
33
+ and "source_hash" in module_metadata
34
+ and module_metadata["source_hash"] != _module.get_module_source_hash()
35
+ ):
36
+ return _module
37
+
38
+ return None
39
+
40
+
41
+ def code_change(plain_module: PlainModule) -> PlainModule | None:
42
+ all_modules = plain_module.all_required_modules + [plain_module]
43
+ for _module in all_modules:
44
+ if len(_module.required_modules) == 0:
45
+ continue
46
+
47
+ module_metadata = _module.load_module_metadata()
48
+ previous_module = _module.required_modules[-1]
49
+ if (
50
+ module_metadata
51
+ and "required_modules_code_hash" in module_metadata
52
+ and module_metadata["required_modules_code_hash"] != previous_module.get_module_code_hash()
53
+ ):
54
+ return previous_module
55
+
56
+ return None
57
+
58
+
59
+ def module_comes_before_or_equal(
60
+ all_required_modules: list[PlainModule],
61
+ module1: PlainModule,
62
+ module2: PlainModule,
63
+ ) -> bool:
64
+ for module in all_required_modules:
65
+ if module.module_name == module1.module_name:
66
+ return True
67
+ if module.module_name == module2.module_name:
68
+ return False
69
+
70
+ raise ValueError(f"Module {module1.module_name} and {module2.module_name} not found in {all_required_modules}")
71
+
72
+
73
+ def get_plain_module_render_state(plain_module: PlainModule) -> PlainModuleRenderState | None:
74
+ sc = spec_change(plain_module)
75
+ cc = code_change(plain_module)
76
+ all_required_modules = plain_module.all_required_modules
77
+ last_rendered_module_name, last_rendered_frid = plain_module.get_module_render_status()
78
+ if last_rendered_module_name is None and last_rendered_frid is None:
79
+ return None
80
+
81
+ if last_rendered_module_name == plain_module.module_name:
82
+ module = plain_module
83
+ else:
84
+ found_module: PlainModule | None = None
85
+ for required_module in all_required_modules:
86
+ if required_module.module_name == last_rendered_module_name:
87
+ found_module = required_module
88
+
89
+ if found_module is None:
90
+ raise ModuleDoesNotExistError(
91
+ f"Last rendered module {last_rendered_module_name} not found in {[rmodule.module_name for rmodule in all_required_modules]}"
92
+ )
93
+ module = found_module
94
+
95
+ pr = PlainModuleRenderState(
96
+ last_render_module=module,
97
+ last_render_frid=last_rendered_frid,
98
+ change=None,
99
+ change_type=None,
100
+ )
101
+
102
+ if sc is None and cc is None:
103
+ return pr
104
+
105
+ if sc is not None:
106
+ pr.change = sc
107
+ pr.change_type = "spec_change"
108
+
109
+ if cc is not None and (
110
+ pr.change is None
111
+ or (pr.change is not None and module_comes_before_or_equal(all_required_modules, cc, pr.change))
112
+ ):
113
+ pr.change = cc
114
+ pr.change_type = "code_change"
115
+
116
+ return pr
117
+
118
+
119
+ def get_all_affected_modules_from_change(
120
+ plain_module: PlainModule,
121
+ plain_module_render_state: PlainModuleRenderState,
122
+ ) -> list[PlainModule]:
123
+ all_affected_modules = dict[str, PlainModule]()
124
+
125
+ if plain_module_render_state.change_type == "spec_change":
126
+ start_module = plain_module_render_state.change
127
+ elif plain_module_render_state.change_type == "code_change":
128
+ if plain_module_render_state.change.is_module_fully_rendered():
129
+ start_module = plain_module.get_next_module(plain_module_render_state.change.module_name)
130
+ else:
131
+ start_module = plain_module_render_state.change
132
+ else:
133
+ raise ValueError(f"Unknown change type: {plain_module_render_state.change_type}")
134
+
135
+ affected_module = False
136
+ all_modules = plain_module.all_required_modules + [plain_module]
137
+ for module in all_modules:
138
+ if module.module_name == start_module.module_name:
139
+ affected_module = True
140
+
141
+ if affected_module and module.module_name not in all_affected_modules:
142
+ all_affected_modules[module.module_name] = module
143
+
144
+ return list(all_affected_modules.values())
145
+
146
+
147
+ def change_is_only_future_work(
148
+ plain_module: PlainModule,
149
+ plain_module_render_state: PlainModuleRenderState,
150
+ partial_start: "PartialRenderStart | None",
151
+ ) -> bool:
152
+ """Return True when a spec change only adds work *after* the last-rendered functionality.
153
+
154
+ Appending a new functionality past the render frontier (e.g. a new functionality at the
155
+ end of the last module) does not affect anything already rendered, so it needs no user
156
+ decision. The normal "continue" path renders the outstanding functionalities, starting
157
+ from the first one that was never rendered.
158
+ """
159
+ if partial_start is None:
160
+ return False
161
+
162
+ all_modules = plain_module.all_required_modules + [plain_module]
163
+ order = {module.module_name: index for index, module in enumerate(all_modules)}
164
+ last_index = order[plain_module_render_state.last_render_module.module_name]
165
+ start_index = order[partial_start.module.module_name]
166
+
167
+ if start_index != last_index:
168
+ return start_index > last_index
169
+
170
+ if plain_module_render_state.last_render_frid is None:
171
+ return True
172
+
173
+ return int(partial_start.frid) > int(plain_module_render_state.last_render_frid)
174
+
175
+
176
+ def _resume_render_choice(
177
+ plain_module: PlainModule,
178
+ plain_module_render_state: PlainModuleRenderState,
179
+ force_render: bool,
180
+ ) -> RenderChoice:
181
+ """The natural next step from the last-rendered position when no decision is needed:
182
+ start an unrendered module, continue a partially-rendered one, or advance to the next.
183
+ """
184
+ pr = plain_module_render_state
185
+
186
+ if pr.last_render_module.has_no_rendered_functionality():
187
+ return RenderChoice(
188
+ module=pr.last_render_module,
189
+ render_range=None,
190
+ choice_type="module_start",
191
+ wipe_later_modules=True,
192
+ is_destructive=False,
193
+ )
194
+
195
+ if not pr.last_render_module.is_module_fully_rendered():
196
+ if not pr.last_render_frid:
197
+ raise ValueError("Last render FRID is not set for a non-initial module")
198
+
199
+ next_frid, next_module = plain_module.get_next_frid(pr.last_render_frid, pr.last_render_module.module_name)
200
+ render_range = plain_spec.get_render_range_from(next_frid, next_module.plain_source)
201
+ return RenderChoice(
202
+ module=next_module,
203
+ render_range=None if force_render else render_range,
204
+ wipe_later_modules=force_render,
205
+ choice_type="continue_from_frid",
206
+ )
207
+
208
+ next_module = plain_module.get_next_module(pr.last_render_module.module_name) or pr.last_render_module
209
+ return RenderChoice(
210
+ module=next_module,
211
+ render_range=None,
212
+ choice_type="module_start",
213
+ is_destructive=pr.last_render_module.module_name == plain_module.module_name,
214
+ )
215
+
216
+
217
+ def get_render_choices(
218
+ plain_module: PlainModule,
219
+ plain_module_render_state: PlainModuleRenderState,
220
+ force_render: bool = False,
221
+ ) -> dict[str, RenderChoice]:
222
+ render_choices = list[RenderChoice]()
223
+ module_start_points = list[str]()
224
+
225
+ # Decide whether the detected change actually requires a decision. A spec change that only
226
+ # adds work *after* the last-rendered functionality (e.g. a new functionality appended to
227
+ # the end of the last module) does not affect anything already rendered, so it is treated
228
+ # like a normal "continue" rather than a change that requires choosing where to restart.
229
+ partial_start = None
230
+ treat_as_continue = plain_module_render_state.change is None
231
+ if plain_module_render_state.change is not None and plain_module_render_state.change_type == "spec_change":
232
+ partial_start = determine_partial_render_start(plain_module)
233
+ if change_is_only_future_work(plain_module, plain_module_render_state, partial_start):
234
+ treat_as_continue = True
235
+
236
+ if treat_as_continue:
237
+ # Continue / resume from the last-rendered position. The change-driven blocks below decide
238
+ # the start otherwise — once a change touches already-rendered work, resuming from the old
239
+ # position would build on stale code.
240
+ render_choices.append(_resume_render_choice(plain_module, plain_module_render_state, force_render))
241
+
242
+ else:
243
+ # change is set here (otherwise treat_as_continue would be True), so change_type is always
244
+ # "spec_change" or "code_change" and get_all_affected_modules_from_change is well-defined.
245
+ all_affected_modules = get_all_affected_modules_from_change(plain_module, plain_module_render_state)
246
+
247
+ if plain_module_render_state.change_type == "spec_change" and partial_start is not None:
248
+ # Optimized partial render: render from the changed functionality, keeping the
249
+ # module's earlier (unchanged) functionalities.
250
+ render_range = plain_spec.get_render_range_from(partial_start.frid, partial_start.module.plain_source)
251
+ render_choices.append(
252
+ RenderChoice(
253
+ module=partial_start.module,
254
+ render_range=render_range,
255
+ choice_type="render_from_change",
256
+ wipe_later_modules=len(all_affected_modules) > 1,
257
+ is_destructive=True,
258
+ )
259
+ )
260
+
261
+ # Full re-render of the affected module(s) from scratch. For a code change this is always
262
+ # the action; for a spec change with no partial start (non-FR sections changed, or only
263
+ # trailing FR removals) it is the only safe option, and when a partial start does exist it
264
+ # is offered alongside it as the "rebuild this module cleanly" alternative. The "re-render
265
+ # from first module" choice below remains the full-chain reset.
266
+ if len(all_affected_modules) > 0 and all_affected_modules[0].module_name not in module_start_points:
267
+ render_choices.append(
268
+ RenderChoice(
269
+ module=all_affected_modules[0],
270
+ render_range=None,
271
+ choice_type="rerender_affected",
272
+ wipe_later_modules=True,
273
+ is_destructive=True,
274
+ )
275
+ )
276
+ module_start_points.append(all_affected_modules[0].module_name)
277
+
278
+ if len(plain_module.all_required_modules) > 0:
279
+ first_module = plain_module.all_required_modules[0]
280
+ if first_module.module_name != plain_module_render_state.last_render_module.module_name and (
281
+ plain_module_render_state.change is not None
282
+ and first_module.module_name != plain_module_render_state.change.module_name
283
+ and first_module.module_name not in module_start_points
284
+ ):
285
+ render_choices.append(
286
+ RenderChoice(
287
+ module=first_module,
288
+ render_range=None,
289
+ choice_type="rerender_from_first",
290
+ wipe_later_modules=True,
291
+ is_destructive=True,
292
+ )
293
+ )
294
+ module_start_points.append(first_module.module_name)
295
+
296
+ render_choices.append(RenderChoice(module=None, render_range=None, choice_type="quit"))
297
+ return {str(idx): choice for idx, choice in enumerate(render_choices, start=1)}