codeplain 0.3.11.dev17__tar.gz → 0.3.11.dev19__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 (281) hide show
  1. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/PKG-INFO +1 -1
  2. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/codeplain_REST_api.py +27 -1
  3. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/file_utils.py +14 -0
  4. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code.py +61 -5
  5. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain_file.py +17 -9
  6. codeplain-0.3.11.dev19/tests/data/plainfile/plain_source_with_missing_link.plain +9 -0
  7. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/task_manager_with_reference_links.plain +2 -2
  8. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_plainfileparser.py +43 -4
  9. codeplain-0.3.11.dev19/tests/test_render_finished_report.py +200 -0
  10. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.claude/settings.json +0 -0
  11. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.flake8 +0 -0
  12. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.github/dependabot.yml +0 -0
  13. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.github/workflows/e2e.yml +0 -0
  14. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.github/workflows/lint-and-test.yml +0 -0
  15. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.github/workflows/lint-powershell.yml +0 -0
  16. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.github/workflows/nofity-slack-on-main-merge.yml +0 -0
  17. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.github/workflows/publish-dev-to-pypi.yml +0 -0
  18. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.github/workflows/publish-install-script.yml +0 -0
  19. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.github/workflows/publish-to-pypi.yml +0 -0
  20. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.gitignore +0 -0
  21. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.vscode/launch.json +0 -0
  22. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/.vscode/settings.json +0 -0
  23. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/CLAUDE.md +0 -0
  24. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/CONTRIBUTING.md +0 -0
  25. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/LICENSE +0 -0
  26. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/README.md +0 -0
  27. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/change_detection.py +0 -0
  28. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/cli_output/__init__.py +0 -0
  29. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/cli_output/dry_run.py +0 -0
  30. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/cli_output/render_summary.py +0 -0
  31. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/cli_output/status.py +0 -0
  32. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/concept_utils.py +0 -0
  33. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/config/__init__.py +0 -0
  34. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/config/system_config.yaml +0 -0
  35. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/conformance_fix_journal.py +0 -0
  36. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/diff_utils.py +0 -0
  37. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/docs/generate_cli.py +0 -0
  38. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/docs/plain2code_cli.md +0 -0
  39. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/docs/starting_a_plain_project_from_scratch.md +0 -0
  40. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/event_bus.py +0 -0
  41. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_golang/config.yaml +0 -0
  42. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_golang/harness_tests/hello_world_test.go +0 -0
  43. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_golang/hello_world_golang.plain +0 -0
  44. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_golang/run.sh +0 -0
  45. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_python/config.yaml +0 -0
  46. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py +0 -0
  47. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_python/hello_world_python.plain +0 -0
  48. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_python/run.sh +0 -0
  49. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_react/config.yaml +0 -0
  50. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/e2e/hello_world.cy.ts +0 -0
  51. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/support/e2e.ts +0 -0
  52. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress.config.ts +0 -0
  53. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_react/harness_tests/hello_world_display/package.json +0 -0
  54. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_react/harness_tests/hello_world_display/tsconfig.json +0 -0
  55. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_react/hello_world_react.plain +0 -0
  56. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/example_hello_world_react/run.sh +0 -0
  57. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/examples/run.sh +0 -0
  58. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/git_preflight.py +0 -0
  59. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/git_utils.py +0 -0
  60. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/install/bash/examples.sh +0 -0
  61. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/install/bash/install.sh +0 -0
  62. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/install/bash/walkthrough.sh +0 -0
  63. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/install/powershell/examples.ps1 +0 -0
  64. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/install/powershell/install.ps1 +0 -0
  65. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/install/powershell/walkthrough.ps1 +0 -0
  66. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/memory_management.py +0 -0
  67. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/metadata_utils.py +0 -0
  68. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/module_renderer.py +0 -0
  69. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/partial_rendering.py +0 -0
  70. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/path_resolution.py +0 -0
  71. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_arguments.py +0 -0
  72. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_cli.md +0 -0
  73. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_console.py +0 -0
  74. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_events.py +0 -0
  75. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_exceptions.py +0 -0
  76. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_logger.py +0 -0
  77. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_nodes.py +0 -0
  78. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_read_config.py +0 -0
  79. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_state.py +0 -0
  80. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_telemetry.py +0 -0
  81. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain2code_utils.py +0 -0
  82. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain_modules.py +0 -0
  83. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/plain_spec.py +0 -0
  84. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/pyproject.toml +0 -0
  85. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/pytest.ini +0 -0
  86. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/__init__.py +0 -0
  87. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/analyze_specification_ambiguity.py +0 -0
  88. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/base_action.py +0 -0
  89. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/commit_conformance_tests_changes.py +0 -0
  90. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/commit_implementation_code_changes.py +0 -0
  91. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/create_dist.py +0 -0
  92. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/distill_conformance_test_memory.py +0 -0
  93. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/exit_with_error.py +0 -0
  94. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/finish_functional_requirement.py +0 -0
  95. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/fix_conformance_test.py +0 -0
  96. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/fix_unit_tests.py +0 -0
  97. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/prepare_repositories.py +0 -0
  98. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/prepare_testing_environment.py +0 -0
  99. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/refactor_code.py +0 -0
  100. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/render_conformance_tests.py +0 -0
  101. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/render_functional_requirement.py +0 -0
  102. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/run_conformance_tests.py +0 -0
  103. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/run_unit_tests.py +0 -0
  104. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/actions/summarize_conformance_tests.py +0 -0
  105. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/code_renderer.py +0 -0
  106. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/conformance_tests.py +0 -0
  107. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/implementation_code_helpers.py +0 -0
  108. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/render_context.py +0 -0
  109. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/render_types.py +0 -0
  110. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/render_utils.py +0 -0
  111. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/state_machine_config.py +0 -0
  112. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/states.py +0 -0
  113. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/render_machine/triggers.py +0 -0
  114. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/requirements.txt +0 -0
  115. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/resources/codeplain_overview.png +0 -0
  116. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/resources/plain_example.png +0 -0
  117. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/standard_template_library/golang-console-app-template.plain +0 -0
  118. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/standard_template_library/python-console-app-template.plain +0 -0
  119. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/standard_template_library/typescript-react-app-boilerplate.plain +0 -0
  120. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/standard_template_library/typescript-react-app-template.plain +0 -0
  121. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/system_config.py +0 -0
  122. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_conformance_tests_cypress.ps1 +0 -0
  123. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_conformance_tests_cypress.sh +0 -0
  124. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_conformance_tests_golang.ps1 +0 -0
  125. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_conformance_tests_golang.sh +0 -0
  126. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_conformance_tests_python.ps1 +0 -0
  127. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_conformance_tests_python.sh +0 -0
  128. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_unittests_flutter.ps1 +0 -0
  129. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_unittests_flutter.sh +0 -0
  130. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_unittests_golang.ps1 +0 -0
  131. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_unittests_golang.sh +0 -0
  132. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_unittests_python.ps1 +0 -0
  133. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_unittests_python.sh +0 -0
  134. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_unittests_react.ps1 +0 -0
  135. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/test_scripts/run_unittests_react.sh +0 -0
  136. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/__init__.py +0 -0
  137. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/conftest.py +0 -0
  138. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/acceptance_tests_warning/main_requiring_acceptance_tests.plain +0 -0
  139. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/acceptance_tests_warning/required_with_acceptance_tests.plain +0 -0
  140. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/circular_imports_1.plain +0 -0
  141. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/circular_imports_2.plain +0 -0
  142. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/circular_imports_main.plain +0 -0
  143. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/diamond_import_1.plain +0 -0
  144. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/diamond_import_2.plain +0 -0
  145. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/diamond_import_common.plain +0 -0
  146. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/diamond_imports_main.plain +0 -0
  147. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/import_with_functionalities.plain +0 -0
  148. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
  149. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/imports/non_existent_import.plain +0 -0
  150. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/partial_rendering/pr_code_var.plain +0 -0
  151. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/partial_rendering/pr_implement.plain +0 -0
  152. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/partial_rendering/pr_leaf.plain +0 -0
  153. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/partial_rendering/pr_middle.plain +0 -0
  154. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/partial_rendering/pr_root.plain +0 -0
  155. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/partial_rendering/pr_solo.plain +0 -0
  156. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  157. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  158. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
  159. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  160. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  161. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
  162. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  163. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  164. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  165. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  166. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  167. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  168. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  169. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  170. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  171. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
  172. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  173. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  174. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
  175. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  176. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  177. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
  178. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  179. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  180. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
  181. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  182. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
  183. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
  184. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
  185. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
  186. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
  187. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
  188. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
  189. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  190. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  191. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
  192. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
  193. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
  194. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  195. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  196. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  197. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  198. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  199. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  200. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/required_concepts.plain +0 -0
  201. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  202. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
  203. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  204. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  205. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  206. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  207. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  208. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  209. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
  210. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/topological_sort.plain +0 -0
  211. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  212. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/circular_requires_main.plain +0 -0
  213. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/circular_requires_sub.plain +0 -0
  214. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/diamond_requires_1.plain +0 -0
  215. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/diamond_requires_2.plain +0 -0
  216. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/diamond_requires_common.plain +0 -0
  217. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/diamond_requires_main.plain +0 -0
  218. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/independent_requires_1.plain +0 -0
  219. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/independent_requires_2.plain +0 -0
  220. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/independent_requires_main.plain +0 -0
  221. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/non_existent_require.plain +0 -0
  222. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/normal_requires_1.plain +0 -0
  223. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/normal_requires_2.plain +0 -0
  224. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/normal_requires_common.plain +0 -0
  225. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/requires/normal_requires_main.plain +0 -0
  226. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/sample_base64_image.txt +0 -0
  227. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/simple.plain +0 -0
  228. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/templates/block_level_include.plain +0 -0
  229. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/templates/code_variables.plain +0 -0
  230. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/templates/header.plain +0 -0
  231. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/templates/implement.plain +0 -0
  232. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/templates/implement_2.plain +0 -0
  233. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/templates/template_include.plain +0 -0
  234. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/data/templates/test_hardest_problem.plain +0 -0
  235. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/e2e/Dockerfile +0 -0
  236. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/e2e/conftest.py +0 -0
  237. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/e2e/test_hello_world_python.py +0 -0
  238. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/e2e/test_hello_world_python_windows.py +0 -0
  239. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_arg_provenance.py +0 -0
  240. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_change_detection.py +0 -0
  241. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_cli_output.py +0 -0
  242. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_conformance_fix_journal.py +0 -0
  243. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_console_log_styles.py +0 -0
  244. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_distill_conformance_test_memory.py +0 -0
  245. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_dry_run_link_validation.py +0 -0
  246. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_file_utils.py +0 -0
  247. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_fix_unit_tests_conformance_context.py +0 -0
  248. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_folder_path_resolution.py +0 -0
  249. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_git_preflight.py +0 -0
  250. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_git_utils.py +0 -0
  251. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_headless_logging.py +0 -0
  252. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_imports.py +0 -0
  253. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_log_file_name_resolution.py +0 -0
  254. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_logging_config_path_resolution.py +0 -0
  255. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_partial_rendering.py +0 -0
  256. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_path_resolution.py +0 -0
  257. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_plain2code.py +0 -0
  258. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_plain2code_state.py +0 -0
  259. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_plain2code_utils.py +0 -0
  260. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_plain_modules.py +0 -0
  261. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_plainfile.py +0 -0
  262. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_plainspec.py +0 -0
  263. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_prepare_repositories_layout.py +0 -0
  264. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_requires.py +0 -0
  265. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_resolve_config_file.py +0 -0
  266. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_script_path_resolution.py +0 -0
  267. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_system_config.py +0 -0
  268. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_telemetry.py +0 -0
  269. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_tui_components.py +0 -0
  270. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_tui_usage.py +0 -0
  271. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tests/test_usage_summary.py +0 -0
  272. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tui/__init__.py +0 -0
  273. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tui/components.py +0 -0
  274. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tui/models.py +0 -0
  275. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tui/plain2code_tui.py +0 -0
  276. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tui/plain_module_render_choice_tui.py +0 -0
  277. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tui/spinner.py +0 -0
  278. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tui/state_handlers.py +0 -0
  279. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tui/styles.css +0 -0
  280. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/tui/widget_helpers.py +0 -0
  281. {codeplain-0.3.11.dev17 → codeplain-0.3.11.dev19}/usage_summary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeplain
3
- Version: 0.3.11.dev17
3
+ Version: 0.3.11.dev19
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -11,6 +11,10 @@ from plain2code_state import RunState
11
11
  MAX_RETRIES = 4
12
12
  RETRY_DELAY = 3
13
13
 
14
+ # A report the render does not consume must not be able to hold the process open
15
+ # at exit, and requests waits indefinitely by default.
16
+ REPORT_TIMEOUT_SECONDS = 5
17
+
14
18
  RETRY_ERROR_CODES = [
15
19
  "LLMInternalError",
16
20
  ]
@@ -125,6 +129,7 @@ class CodeplainAPI:
125
129
  run_state: Optional[RunState],
126
130
  num_retries: int = MAX_RETRIES,
127
131
  silent: bool = False,
132
+ timeout: Optional[int] = None,
128
133
  ):
129
134
  if run_state is not None:
130
135
  self._extend_payload_with_run_state(payload, run_state)
@@ -134,7 +139,7 @@ class CodeplainAPI:
134
139
 
135
140
  for attempt in range(num_retries + 1):
136
141
  try:
137
- response = requests.post(endpoint_url, headers=headers, json=payload)
142
+ response = requests.post(endpoint_url, headers=headers, json=payload, timeout=timeout)
138
143
 
139
144
  try:
140
145
  response_json = response.json()
@@ -173,6 +178,27 @@ class CodeplainAPI:
173
178
  }
174
179
  return self.post_request(endpoint_url, headers, payload, None, num_retries=0, silent=True)
175
180
 
181
+ def render_finished(self, outcome: str, run_state: RunState):
182
+ """Report the terminal state a render reached.
183
+
184
+ Called once per render, whatever the outcome. The render state machine
185
+ lives here, so the API cannot tell a finished render from one still
186
+ going; it has to be told.
187
+
188
+ No retries and no console output: a report has no result the render
189
+ needs, and must not add minutes to a render that has already ended nor
190
+ print retry noise over the exit summary.
191
+ """
192
+ endpoint_url = f"{self.api_url}/render_finished"
193
+ headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
194
+ payload = {
195
+ "outcome": outcome,
196
+ }
197
+
198
+ return self.post_request(
199
+ endpoint_url, headers, payload, run_state, num_retries=0, silent=True, timeout=REPORT_TIMEOUT_SECONDS
200
+ )
201
+
176
202
  def render_functional_requirement(
177
203
  self,
178
204
  frid: str,
@@ -180,6 +180,20 @@ def open_from(dirs, file_name):
180
180
  return None
181
181
 
182
182
 
183
+ def resolve_linked_resource(dirs, file_name):
184
+ """Return the first existing path for file_name across dirs, or None if not found.
185
+
186
+ Mirrors the search order used by open_from so that validating a linked resource and
187
+ loading it can never disagree.
188
+ """
189
+ for dir in dirs:
190
+ full_file_name = os.path.join(dir, file_name)
191
+ if os.path.exists(full_file_name):
192
+ return full_file_name
193
+
194
+ return None
195
+
196
+
183
197
  def load_linked_resources(template_dirs: list[str], resources_list, module_name: str):
184
198
  linked_resources = {}
185
199
 
@@ -61,6 +61,11 @@ from tui.plain_module_render_choice_tui import PlainModuleRenderChoiceTUI
61
61
  DEFAULT_TEMPLATE_DIRS = "standard_template_library"
62
62
  RENDER_THREAD_SHUTDOWN_TIMEOUT = 0.7
63
63
 
64
+ # The terminal states a render can reach, as the API names them.
65
+ RENDER_OUTCOME_COMPLETED = "completed"
66
+ RENDER_OUTCOME_FAILED = "failed"
67
+ RENDER_OUTCOME_CANCELLED = "cancelled"
68
+
64
69
  # Exceptions that represent expected, user-facing error conditions. They are
65
70
  # reported to the user directly and must never be sent to Sentry as crashes.
66
71
  EXPECTED_EXCEPTIONS = (
@@ -177,6 +182,52 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI) -> Optional[str]
177
182
  return response.get("user_email")
178
183
 
179
184
 
185
+ def _render_outcome(run_state: RunState, interrupted: bool = False) -> str:
186
+ """Name the terminal state a render reached.
187
+
188
+ Ctrl-C is the user stopping the render, so it is a cancellation. It does not
189
+ set render_cancelled: the exit summary reads that flag, and its wording for
190
+ an interrupt stays as it is.
191
+
192
+ Cancellation is tested first. A cancelled render leaves render_succeeded
193
+ False, so asking about success first would report it as a failure.
194
+ """
195
+ if interrupted or run_state.render_cancelled:
196
+ return RENDER_OUTCOME_CANCELLED
197
+ if run_state.render_succeeded:
198
+ return RENDER_OUTCOME_COMPLETED
199
+ return RENDER_OUTCOME_FAILED
200
+
201
+
202
+ def report_render_finished(
203
+ codeplainAPI: codeplain_api.CodeplainAPI, run_state: RunState, interrupted: bool = False
204
+ ) -> None:
205
+ """Tell the API which terminal state this render reached.
206
+
207
+ The API cannot work it out for itself - the state machine lives here - and
208
+ main()'s finally is the one place that sees every ending: success, failure,
209
+ cancellation, an unexpected crash and a keyboard interrupt alike. The state
210
+ machine's own completed and failed hooks see only two of the five.
211
+
212
+ Reported only once the connection check has set a user email, which is
213
+ exactly the condition "the API knows who we are". A missing key, an invalid
214
+ key and an outdated client all leave it unset, and none of them got as far
215
+ as a render.
216
+
217
+ Never raises. A report must not change the exit code, and must not mask the
218
+ error that ended the render.
219
+ """
220
+ if not run_state.user_email:
221
+ return
222
+
223
+ try:
224
+ codeplainAPI.render_finished(_render_outcome(run_state, interrupted), run_state)
225
+ except Exception:
226
+ # Deliberately silent: logging has stopped by now, and a failed report
227
+ # is not the user's problem.
228
+ pass
229
+
230
+
180
231
  def warn_if_acceptance_tests_without_conformance_script(plain_module, args) -> None:
181
232
  """Warn when any loaded module (including required modules) defines acceptance tests
182
233
  but no conformance tests script is configured.
@@ -214,6 +265,7 @@ def validate_linked_resources(plain_module) -> None:
214
265
 
215
266
  def render( # noqa: C901
216
267
  plain_module: plain_modules.PlainModule,
268
+ codeplainAPI: codeplain_api.CodeplainAPI,
217
269
  args,
218
270
  run_state: RunState,
219
271
  event_bus: EventBus,
@@ -224,10 +276,6 @@ def render( # noqa: C901
224
276
  if args.render_range or args.render_from:
225
277
  render_range = plain_spec.compute_render_range(args, plain_module.plain_source)
226
278
 
227
- codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console)
228
- assert args.api is not None and args.api != "", "API URL is required"
229
- codeplainAPI.api_url = args.api
230
-
231
279
  run_state.user_email = _check_connection(codeplainAPI)
232
280
 
233
281
  stop_event = threading.Event()
@@ -419,6 +467,10 @@ def main(): # noqa: C901
419
467
  if not args.api:
420
468
  args.api = system_config.default_api_url
421
469
 
470
+ assert args.api is not None and args.api != "", "API URL is required"
471
+ codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console)
472
+ codeplainAPI.api_url = args.api
473
+
422
474
  run_state = RunState(spec_filename=args.filename, replay_with=args.replay_with)
423
475
 
424
476
  if args.headless:
@@ -431,6 +483,7 @@ def main(): # noqa: C901
431
483
 
432
484
  exc_info = None
433
485
  error_message = None
486
+ interrupted = False
434
487
 
435
488
  try:
436
489
  # Validate API key is present
@@ -438,10 +491,11 @@ def main(): # noqa: C901
438
491
  raise MissingAPIKey(
439
492
  "Your API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n"
440
493
  )
441
- render(plain_module, args, run_state, event_bus, default_log_level)
494
+ render(plain_module, codeplainAPI, args, run_state, event_bus, default_log_level)
442
495
  except BaseException as e:
443
496
  if isinstance(e, KeyboardInterrupt):
444
497
  error_message = "Keyboard interrupt"
498
+ interrupted = True
445
499
  else:
446
500
  error_message = str(e) if str(e) else repr(e)
447
501
 
@@ -460,6 +514,8 @@ def main(): # noqa: C901
460
514
  # Remove any scratch extractions created for archive-only ("<module>.module") modules.
461
515
  for module in plain_module.all_required_modules + [plain_module]:
462
516
  module.cleanup_scratch()
517
+ # Last, so nothing the user is waiting for sits behind a network call.
518
+ report_render_finished(codeplainAPI, run_state, interrupted)
463
519
 
464
520
  if args.headless and (exc_info is not None or not run_state.render_succeeded):
465
521
  sys.exit(1)
@@ -90,7 +90,7 @@ def remove_quotes(token):
90
90
  token.children = tuple(new_children)
91
91
 
92
92
 
93
- def check_section_for_linked_resources(section):
93
+ def check_section_for_linked_resources(section, template_dirs):
94
94
  linked_resources = []
95
95
  for link in traverse(section, klass=Link):
96
96
  parsed_url = urlparse(link.node.target)
@@ -99,11 +99,19 @@ def check_section_for_linked_resources(section):
99
99
  f"Plain syntax error: Only relative links are allowed (text: {link.node.children[0].content}, target: {link.node.target})."
100
100
  )
101
101
 
102
- if not os.path.exists(link.node.target):
103
- raise PlainSyntaxError(f"Plain syntax error: Link {link.node.target} does not exist.")
102
+ resolved_target = file_utils.resolve_linked_resource(template_dirs, link.node.target)
103
+ if resolved_target is None:
104
+ searched_dirs = "\n".join(f" {position}. {dir}" for position, dir in enumerate(template_dirs, start=1))
105
+ raise PlainSyntaxError(
106
+ f"Plain syntax error: Link {link.node.target} does not exist. "
107
+ f"Linked resources are looked up relative to the following directories "
108
+ f"(highest to lowest precedence):\n{searched_dirs}"
109
+ )
104
110
 
105
- if not os.path.isfile(link.node.target):
106
- raise PlainSyntaxError(f"Plain syntax error: Link {link.node.target} must be a file.")
111
+ if not os.path.isfile(resolved_target):
112
+ raise PlainSyntaxError(
113
+ f"Plain syntax error: Link {link.node.target} must be a file (resolved to {resolved_target})."
114
+ )
107
115
 
108
116
  if len(link.node.children) != 1:
109
117
  raise PlainSyntaxError(f"Plain syntax error: Link must have text specified (link: {link.node.target}).")
@@ -122,15 +130,15 @@ def check_section_for_linked_resources(section):
122
130
  section.linked_resources = linked_resources
123
131
 
124
132
 
125
- def check_for_linked_resources(plain_source):
133
+ def check_for_linked_resources(plain_source, template_dirs):
126
134
  for specification_heading in plain_spec.ALLOWED_SPECIFICATION_HEADINGS:
127
135
  if specification_heading in plain_source and hasattr(plain_source[specification_heading], "children"):
128
136
  for requirement in plain_source[specification_heading].children:
129
- check_section_for_linked_resources(requirement)
137
+ check_section_for_linked_resources(requirement, template_dirs)
130
138
 
131
139
  if hasattr(requirement, plain_spec.ACCEPTANCE_TESTS):
132
140
  for acceptance_test in requirement.acceptance_tests:
133
- check_section_for_linked_resources(acceptance_test)
141
+ check_section_for_linked_resources(acceptance_test, template_dirs)
134
142
 
135
143
 
136
144
  def process_section_code_variables(section, code_variables):
@@ -845,7 +853,7 @@ def plain_file_parser( # noqa: C901
845
853
 
846
854
  process_acceptance_tests(plain_file_parse_result.plain_source)
847
855
 
848
- check_for_linked_resources(plain_file_parse_result.plain_source)
856
+ check_for_linked_resources(plain_file_parse_result.plain_source, template_dirs)
849
857
 
850
858
  marshalled_plain_source = marshall_plain_source(plain_file_parse_result.plain_source)
851
859
 
@@ -0,0 +1,9 @@
1
+ ***definitions***
2
+
3
+ ***implementation reqs***
4
+
5
+ - Implementation should be written in Python.
6
+
7
+ ***functional specs***
8
+
9
+ - Links must point at an existing file [missing resource](resources/missing_resource.yaml).
@@ -29,9 +29,9 @@
29
29
 
30
30
  - Implement the entry point for :App:.
31
31
 
32
- - Show :TaskList:. The details of the user interface are provided in the file [task_list_ui_specification.yaml](tests/data/plainfile/task_list_ui_specification.yaml).
32
+ - Show :TaskList:. The details of the user interface are provided in the file [task_list_ui_specification.yaml](task_list_ui_specification.yaml).
33
33
 
34
- - :User: should be able to add :Task:. Only valid :Task: items can be added. The details of the user interface are provided in the file [add_new_task_modal_specification.yaml](tests/data/plainfile/add_new_task_modal_specification.yaml).
34
+ - :User: should be able to add :Task:. Only valid :Task: items can be added. The details of the user interface are provided in the file [add_new_task_modal_specification.yaml](add_new_task_modal_specification.yaml).
35
35
 
36
36
  - :User: should be able to delete :Task:.
37
37
 
@@ -146,14 +146,30 @@ def test_invalid_absolute_link(get_test_data_path):
146
146
  )
147
147
 
148
148
 
149
+ def test_missing_link_error_lists_searched_directories(get_test_data_path):
150
+ """The error tells the user which directories were searched for the linked resource."""
151
+ plain_file_dir = get_test_data_path("data/plainfile")
152
+
153
+ with pytest.raises(PlainSyntaxError) as exception_info:
154
+ plain_file.plain_file_parser(
155
+ "plain_source_with_missing_link.plain",
156
+ [plain_file_dir],
157
+ )
158
+
159
+ message = str(exception_info.value)
160
+ assert "resources/missing_resource.yaml does not exist" in message
161
+ assert "highest to lowest precedence" in message
162
+ assert plain_file_dir in message
163
+
164
+
149
165
  def test_reference_link_parsing(get_test_data_path):
150
166
  _, plain_sections, _ = plain_file.plain_file_parser(
151
167
  "task_manager_with_reference_links.plain",
152
168
  [get_test_data_path("data/plainfile")],
153
169
  )
154
170
  asserted_resources = [
155
- "tests/data/plainfile/task_list_ui_specification.yaml",
156
- "tests/data/plainfile/add_new_task_modal_specification.yaml",
171
+ "task_list_ui_specification.yaml",
172
+ "add_new_task_modal_specification.yaml",
157
173
  ]
158
174
  for functional_requirement in plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS]:
159
175
  if "linked_resources" not in functional_requirement:
@@ -180,13 +196,36 @@ def test_reference_link_parsing(get_test_data_path):
180
196
  assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][1]["linked_resources"] == [
181
197
  {
182
198
  "text": "task_list_ui_specification.yaml",
183
- "target": "tests/data/plainfile/task_list_ui_specification.yaml",
199
+ "target": "task_list_ui_specification.yaml",
200
+ }
201
+ ]
202
+ assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][2]["linked_resources"] == [
203
+ {
204
+ "text": "add_new_task_modal_specification.yaml",
205
+ "target": "add_new_task_modal_specification.yaml",
206
+ }
207
+ ]
208
+
209
+
210
+ def test_reference_link_parsing_independent_of_working_directory(get_test_data_path, monkeypatch, tmp_path):
211
+ """Linked resources resolve against the plain file's directory, not the invocation cwd."""
212
+ monkeypatch.chdir(tmp_path)
213
+
214
+ _, plain_sections, _ = plain_file.plain_file_parser(
215
+ "task_manager_with_reference_links.plain",
216
+ [get_test_data_path("data/plainfile")],
217
+ )
218
+
219
+ assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][1]["linked_resources"] == [
220
+ {
221
+ "text": "task_list_ui_specification.yaml",
222
+ "target": "task_list_ui_specification.yaml",
184
223
  }
185
224
  ]
186
225
  assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][2]["linked_resources"] == [
187
226
  {
188
227
  "text": "add_new_task_modal_specification.yaml",
189
- "target": "tests/data/plainfile/add_new_task_modal_specification.yaml",
228
+ "target": "add_new_task_modal_specification.yaml",
190
229
  }
191
230
  ]
192
231
 
@@ -0,0 +1,200 @@
1
+ """Tests for reporting a render's terminal state to the API.
2
+
3
+ The render state machine lives in the client, so the API only learns how a
4
+ render ended because main() tells it. Two things are worth proving: that the
5
+ outcome names the right ending, and that main() reports on every path out -
6
+ including the ones the state machine never sees, like a keyboard interrupt.
7
+ """
8
+
9
+ from argparse import Namespace
10
+ from unittest.mock import MagicMock, patch
11
+
12
+ import pytest
13
+
14
+ import plain2code
15
+ from plain2code_exceptions import InvalidAPIKey
16
+ from plain2code_state import RunState
17
+
18
+
19
+ def make_run_state(succeeded=False, cancelled=False, user_email="user@codeplain.ai"):
20
+ run_state = RunState(spec_filename="project.plain")
21
+ run_state.set_render_succeeded(succeeded)
22
+ if cancelled:
23
+ run_state.set_render_cancelled()
24
+ run_state.user_email = user_email
25
+ return run_state
26
+
27
+
28
+ class TestRenderOutcome:
29
+ def test_a_successful_render_is_completed(self):
30
+ assert plain2code._render_outcome(make_run_state(succeeded=True)) == "completed"
31
+
32
+ def test_an_unsuccessful_render_is_failed(self):
33
+ assert plain2code._render_outcome(make_run_state(succeeded=False)) == "failed"
34
+
35
+ def test_a_cancelled_render_is_cancelled(self):
36
+ """A cancelled render leaves render_succeeded False, so cancellation has to
37
+ be asked about first or it would be reported as a failure."""
38
+ assert plain2code._render_outcome(make_run_state(cancelled=True)) == "cancelled"
39
+
40
+ def test_cancellation_wins_over_a_success_already_recorded(self):
41
+ """Cancelling during the dist step leaves both flags set."""
42
+ assert plain2code._render_outcome(make_run_state(succeeded=True, cancelled=True)) == "cancelled"
43
+
44
+ def test_an_interrupted_render_is_cancelled(self):
45
+ """Ctrl-C is the user stopping the render. It does not set the cancelled
46
+ flag, so the outcome has to be told about it."""
47
+ assert plain2code._render_outcome(make_run_state(), interrupted=True) == "cancelled"
48
+
49
+
50
+ class TestReportRenderFinished:
51
+ def test_reports_the_outcome_with_the_render_state(self):
52
+ api = MagicMock()
53
+ run_state = make_run_state(succeeded=True)
54
+
55
+ plain2code.report_render_finished(api, run_state)
56
+
57
+ api.render_finished.assert_called_once_with("completed", run_state)
58
+
59
+ def test_says_nothing_when_the_api_never_identified_us(self):
60
+ """user_email is set by the connection check, so an absent one means a
61
+ missing key, an invalid key or an outdated client - none of which rendered."""
62
+ api = MagicMock()
63
+
64
+ plain2code.report_render_finished(api, make_run_state(user_email=None))
65
+
66
+ api.render_finished.assert_not_called()
67
+
68
+ @pytest.mark.parametrize(
69
+ "failure",
70
+ [
71
+ ConnectionError("api unreachable"),
72
+ TimeoutError("api slow"),
73
+ RuntimeError("something else entirely"),
74
+ ],
75
+ )
76
+ def test_a_failed_report_never_propagates(self, failure):
77
+ """The report is the last thing a render does. It must not become the
78
+ reason the render looks like it failed."""
79
+ api = MagicMock()
80
+ api.render_finished.side_effect = failure
81
+
82
+ plain2code.report_render_finished(api, make_run_state(succeeded=True))
83
+
84
+
85
+ def make_args(api_key="test-key", headless=False):
86
+ return Namespace(
87
+ version=False,
88
+ status=False,
89
+ full_plain=False,
90
+ dry_run=False,
91
+ api_key=api_key,
92
+ api="https://api.example.com",
93
+ filename="project.plain",
94
+ template_dir=None,
95
+ build_folder="build",
96
+ render_range=None,
97
+ render_from=None,
98
+ headless=headless,
99
+ replay_with=None,
100
+ log_to_file=False,
101
+ log_file_name="codeplain.log",
102
+ conformance_tests_script=None,
103
+ unittests_script=None,
104
+ prepare_environment_script=None,
105
+ )
106
+
107
+
108
+ def run_main(render_side_effect=None, api_key="test-key"):
109
+ """Drive main() with everything but the reporting path stubbed out.
110
+
111
+ render() is replaced, so the stub is what decides how the render "ended" -
112
+ which is the point: the report must be made from main()'s finally whatever
113
+ render did, not from inside the state machine.
114
+
115
+ Returns the API client mock so the caller can inspect the report.
116
+ """
117
+ api = MagicMock()
118
+
119
+ def fake_render(plain_module, codeplainAPI, args, run_state, event_bus, default_log_level="INFO"):
120
+ run_state.user_email = "user@codeplain.ai"
121
+ if render_side_effect is not None:
122
+ render_side_effect(run_state)
123
+
124
+ with (
125
+ patch("plain2code.parse_arguments", return_value=make_args(api_key=api_key)),
126
+ patch("plain2code.file_utils.get_template_directories", return_value=["templates"]),
127
+ patch("plain2code.plain_modules.PlainModule", return_value=MagicMock(all_required_modules=[])),
128
+ patch("plain2code.setup_logging", return_value="INFO"),
129
+ patch("plain2code.initialize_telemetry"),
130
+ patch("plain2code.stop_narrating"),
131
+ patch("plain2code.print_exit_summary"),
132
+ patch("plain2code.dump_crash_logs"),
133
+ patch("plain2code.capture_crash"),
134
+ patch("plain2code.codeplain_api.CodeplainAPI", return_value=api),
135
+ patch("plain2code.render", side_effect=fake_render),
136
+ ):
137
+ plain2code.main()
138
+
139
+ return api
140
+
141
+
142
+ class TestMainReportsEveryEnding:
143
+ """main()'s finally is the only place that sees all of these."""
144
+
145
+ def test_a_completed_render_is_reported(self):
146
+ api = run_main(lambda run_state: run_state.set_render_succeeded(True))
147
+
148
+ assert api.render_finished.call_args.args[0] == "completed"
149
+
150
+ def test_a_failed_render_is_reported(self):
151
+ def fail(run_state):
152
+ run_state.set_render_succeeded(False)
153
+ raise Exception("rendering blew up")
154
+
155
+ api = run_main(fail)
156
+
157
+ assert api.render_finished.call_args.args[0] == "failed"
158
+
159
+ def test_an_expected_error_is_reported_as_a_failure(self):
160
+ def fail(run_state):
161
+ raise InvalidAPIKey("key went stale mid-render")
162
+
163
+ api = run_main(fail)
164
+
165
+ assert api.render_finished.call_args.args[0] == "failed"
166
+
167
+ def test_a_cancelled_render_is_reported(self):
168
+ api = run_main(lambda run_state: run_state.set_render_cancelled())
169
+
170
+ assert api.render_finished.call_args.args[0] == "cancelled"
171
+
172
+ def test_a_keyboard_interrupt_is_reported_as_cancelled(self):
173
+ """Ctrl-C never enters a terminal state in the state machine, so this
174
+ ending exists only in main(). In headless mode it is the only way to stop
175
+ a render, and stopping is a cancellation, not a failure."""
176
+
177
+ def interrupt(run_state):
178
+ raise KeyboardInterrupt()
179
+
180
+ api = run_main(interrupt)
181
+
182
+ assert api.render_finished.call_args.args[0] == "cancelled"
183
+
184
+ def test_a_keyboard_interrupt_leaves_the_exit_summary_alone(self):
185
+ """The summary reads render_cancelled. Only the reported outcome changes."""
186
+ captured = {}
187
+
188
+ def interrupt(run_state):
189
+ captured["run_state"] = run_state
190
+ raise KeyboardInterrupt()
191
+
192
+ run_main(interrupt)
193
+
194
+ assert captured["run_state"].render_cancelled is False
195
+
196
+ def test_a_missing_api_key_reports_nothing(self):
197
+ """render() is never reached, so nothing identified the caller."""
198
+ api = run_main(api_key=None)
199
+
200
+ api.render_finished.assert_not_called()