codeplain 0.3.2__tar.gz → 0.3.4__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.
- {codeplain-0.3.2 → codeplain-0.3.4}/PKG-INFO +8 -2
- {codeplain-0.3.2 → codeplain-0.3.4}/README.md +5 -0
- codeplain-0.3.4/_version.py +1 -0
- codeplain-0.3.4/change_detection.py +190 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/cli_output/status.py +14 -9
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_python/hello_world_python.plain +3 -3
- codeplain-0.3.4/partial_rendering.py +297 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code.py +28 -23
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code_state.py +4 -0
- codeplain-0.3.4/plain2code_telemetry.py +114 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain_file.py +8 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain_modules.py +36 -2
- {codeplain-0.3.2 → codeplain-0.3.4}/pyproject.toml +2 -1
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/finish_functional_requirement.py +2 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/code_renderer.py +2 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/render_context.py +23 -44
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/state_machine_config.py +22 -63
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/triggers.py +1 -1
- {codeplain-0.3.2 → codeplain-0.3.4}/requirements.txt +1 -0
- codeplain-0.3.4/tests/data/partial_rendering/pr_code_var.plain +9 -0
- codeplain-0.3.4/tests/data/partial_rendering/pr_implement.plain +2 -0
- codeplain-0.3.4/tests/test_change_detection.py +394 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_cli_output.py +119 -13
- codeplain-0.3.4/tests/test_partial_rendering.py +571 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_plain_modules.py +49 -0
- codeplain-0.3.4/tests/test_telemetry.py +204 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tui/plain_module_render_choice_tui.py +33 -4
- codeplain-0.3.2/.claude/settings.local.json +0 -66
- codeplain-0.3.2/.zed/settings.json +0 -46
- codeplain-0.3.2/_version.py +0 -1
- codeplain-0.3.2/examples/example_hello_world_python/text.txt +0 -1
- codeplain-0.3.2/partial_rendering.py +0 -228
- codeplain-0.3.2/tests/test_partial_rendering.py +0 -316
- {codeplain-0.3.2 → codeplain-0.3.4}/.claude/settings.json +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.flake8 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.github/dependabot.yml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.github/workflows/e2e.yml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.github/workflows/lint-and-test.yml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.github/workflows/nofity-slack-on-main-merge.yml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.github/workflows/publish-install-script.yml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.github/workflows/publish-to-pypi.yml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.gitignore +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.vscode/launch.json +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/.vscode/settings.json +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/CLAUDE.md +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/LICENSE +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/cli_output/__init__.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/cli_output/dry_run.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/cli_output/render_summary.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/codeplain_REST_api.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/concept_utils.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/config/__init__.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/config/system_config.yaml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/diff_utils.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/docs/generate_cli.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/docs/plain2code_cli.md +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/docs/starting_a_plain_project_from_scratch.md +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/event_bus.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_golang/config.yaml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_golang/harness_tests/hello_world_test.go +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_golang/hello_world_golang.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_golang/run.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_python/config.yaml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_python/run.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_react/config.yaml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/e2e/hello_world.cy.ts +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/support/e2e.ts +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress.config.ts +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_react/harness_tests/hello_world_display/package.json +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_react/harness_tests/hello_world_display/tsconfig.json +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_react/hello_world_react.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_react/run.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/examples/run.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/file_utils.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/git_utils.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/install/bash/examples.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/install/bash/install.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/install/bash/walkthrough.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/install/powershell/examples.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/install/powershell/install.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/install/powershell/walkthrough.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/memory_management.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/module_renderer.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code_arguments.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code_console.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code_events.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code_exceptions.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code_logger.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code_nodes.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code_read_config.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain2code_utils.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/plain_spec.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/pytest.ini +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/__init__.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/analyze_specification_ambiguity.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/base_action.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/commit_conformance_tests_changes.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/commit_implementation_code_changes.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/create_dist.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/exit_with_error.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/fix_conformance_test.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/fix_unit_tests.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/prepare_repositories.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/prepare_testing_environment.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/refactor_code.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/render_conformance_tests.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/render_functional_requirement.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/run_conformance_tests.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/run_unit_tests.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/actions/summarize_conformance_tests.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/conformance_tests.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/implementation_code_helpers.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/render_types.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/render_utils.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/render_machine/states.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/resources/codeplain_overview.png +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/resources/plain_example.png +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/standard_template_library/__init__.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/standard_template_library/golang-console-app-template.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/standard_template_library/python-console-app-template.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/standard_template_library/typescript-react-app-boilerplate.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/standard_template_library/typescript-react-app-template.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/system_config.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_conformance_tests_cypress.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_conformance_tests_cypress.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_conformance_tests_golang.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_conformance_tests_golang.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_conformance_tests_python.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_conformance_tests_python.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_unittests_flutter.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_unittests_golang.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_unittests_golang.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_unittests_python.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_unittests_python.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_unittests_react.ps1 +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/test_scripts/run_unittests_react.sh +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/__init__.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/conftest.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/imports/circular_imports_1.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/imports/circular_imports_2.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/imports/circular_imports_main.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/imports/diamond_import_1.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/imports/diamond_import_2.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/imports/diamond_import_common.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/imports/diamond_imports_main.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/imports/non_existent_import.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/partial_rendering/pr_leaf.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/partial_rendering/pr_middle.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/partial_rendering/pr_root.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/partial_rendering/pr_solo.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfile/invalid_specification_order.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts_missing.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts_missing_example.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/required_concepts.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/topological_sort.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/circular_requires_main.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/circular_requires_sub.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/diamond_requires_1.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/diamond_requires_2.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/diamond_requires_common.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/diamond_requires_main.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/independent_requires_1.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/independent_requires_2.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/independent_requires_main.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/non_existent_require.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/normal_requires_1.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/normal_requires_2.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/normal_requires_common.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/requires/normal_requires_main.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/simple.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/templates/block_level_include.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/templates/code_variables.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/templates/header.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/templates/implement.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/templates/implement_2.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/templates/template_include.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/data/templates/test_hardest_problem.plain +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/e2e/Dockerfile +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/e2e/conftest.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/e2e/test_hello_world_python.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/e2e/test_hello_world_python_windows.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_file_utils.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_git_utils.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_imports.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_plainfile.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_plainfileparser.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_plainspec.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_requires.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tests/test_resolve_config_file.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tui/__init__.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tui/components.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tui/models.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tui/plain2code_tui.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tui/spinner.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tui/state_handlers.py +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tui/styles.css +0 -0
- {codeplain-0.3.2 → codeplain-0.3.4}/tui/widget_helpers.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeplain
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Transform plain language specifications into working code
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Classifier: Environment :: Console
|
|
7
7
|
Classifier: Intended Audience :: Developers
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Topic :: Software Development :: Code Generators
|
|
10
|
-
Requires-Python:
|
|
10
|
+
Requires-Python: ==3.11
|
|
11
11
|
Requires-Dist: gitpython==3.1.50
|
|
12
12
|
Requires-Dist: mistletoe==1.3.0
|
|
13
13
|
Requires-Dist: networkx==3.6.1
|
|
@@ -16,6 +16,7 @@ Requires-Dist: python-liquid2==0.3.0
|
|
|
16
16
|
Requires-Dist: pyyaml==6.0.3
|
|
17
17
|
Requires-Dist: requests==2.34.0
|
|
18
18
|
Requires-Dist: rich==15.0.0
|
|
19
|
+
Requires-Dist: sentry-sdk==2.62.0
|
|
19
20
|
Requires-Dist: textual>=7.5.0
|
|
20
21
|
Requires-Dist: tiktoken==0.12.0
|
|
21
22
|
Requires-Dist: transitions==0.9.3
|
|
@@ -103,6 +104,11 @@ After completing the installation steps above, you can immediately test the syst
|
|
|
103
104
|
python hello_world.py
|
|
104
105
|
```
|
|
105
106
|
|
|
107
|
+
### Crash reporting
|
|
108
|
+
|
|
109
|
+
If the plain2code client crashes unexpectedly, it sends an anonymous crash report to Codeplain to help improve the tool. To disable crash reporting, set the `CODEPLAIN_NO_TELEMETRY=1` flag.
|
|
110
|
+
|
|
111
|
+
|
|
106
112
|
## Releasing
|
|
107
113
|
|
|
108
114
|
Releases are built and published with [uv](https://docs.astral.sh/uv/). The version is read from `_version.py`.
|
|
@@ -74,6 +74,11 @@ After completing the installation steps above, you can immediately test the syst
|
|
|
74
74
|
python hello_world.py
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
### Crash reporting
|
|
78
|
+
|
|
79
|
+
If the plain2code client crashes unexpectedly, it sends an anonymous crash report to Codeplain to help improve the tool. To disable crash reporting, set the `CODEPLAIN_NO_TELEMETRY=1` flag.
|
|
80
|
+
|
|
81
|
+
|
|
77
82
|
## Releasing
|
|
78
83
|
|
|
79
84
|
Releases are built and published with [uv](https://docs.astral.sh/uv/). The version is read from `_version.py`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.4"
|
|
@@ -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)
|
|
@@ -50,8 +50,8 @@ def _display_credit_line(plan_credits: dict) -> None:
|
|
|
50
50
|
console.print(f" {plan_label:10} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def
|
|
54
|
-
"""Display a
|
|
53
|
+
def _display_bucket_credit_line(bucket: dict, label: str) -> None:
|
|
54
|
+
"""Display a credit bucket line (purchased or promo) with progress bar."""
|
|
55
55
|
remaining = bucket["remaining"]
|
|
56
56
|
total = bucket["total"]
|
|
57
57
|
expiry_date = bucket["expiry_date"]
|
|
@@ -71,12 +71,12 @@ def _display_purchased_credit_line(bucket: dict) -> None:
|
|
|
71
71
|
bar = _create_progress_bar(remaining, total, width=30)
|
|
72
72
|
|
|
73
73
|
if is_expired:
|
|
74
|
-
console.print(f"
|
|
74
|
+
console.print(f" {label:10} {bar} expired {formatted_date}")
|
|
75
75
|
else:
|
|
76
|
-
console.print(f"
|
|
76
|
+
console.print(f" {label:10} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def _display_status_message(plan_credits: Optional[dict], purchased_credits: list) -> None:
|
|
79
|
+
def _display_status_message(plan_credits: Optional[dict], purchased_credits: list, promo_credits: list) -> None:
|
|
80
80
|
"""Display appropriate status message based on credit state."""
|
|
81
81
|
has_remaining = False
|
|
82
82
|
|
|
@@ -93,8 +93,8 @@ def _display_status_message(plan_credits: Optional[dict], purchased_credits: lis
|
|
|
93
93
|
if remaining > 0 and period_end > now:
|
|
94
94
|
has_remaining = True
|
|
95
95
|
|
|
96
|
-
# Check if any purchased credits have remaining balance and not expired
|
|
97
|
-
for bucket in purchased_credits:
|
|
96
|
+
# Check if any purchased or promo credits have remaining balance and not expired
|
|
97
|
+
for bucket in [*purchased_credits, *promo_credits]:
|
|
98
98
|
dt_str = bucket["expiry_date"].replace("Z", "+00:00")
|
|
99
99
|
expiry_date = datetime.fromisoformat(dt_str)
|
|
100
100
|
# If naive datetime, assume UTC
|
|
@@ -127,6 +127,7 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:
|
|
|
127
127
|
org_owner = response.get("organization_owner_email")
|
|
128
128
|
plan_credits = response.get("plan_credits")
|
|
129
129
|
purchased_credits = response.get("purchased_credits", [])
|
|
130
|
+
promo_credits = response.get("promo_credits", [])
|
|
130
131
|
|
|
131
132
|
# Display header information
|
|
132
133
|
if client_version_valid:
|
|
@@ -153,8 +154,12 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:
|
|
|
153
154
|
|
|
154
155
|
# Display purchased credits
|
|
155
156
|
for bucket in purchased_credits:
|
|
156
|
-
|
|
157
|
+
_display_bucket_credit_line(bucket, "Purchased")
|
|
158
|
+
|
|
159
|
+
# Display promo credits
|
|
160
|
+
for bucket in promo_credits:
|
|
161
|
+
_display_bucket_credit_line(bucket, "Promo")
|
|
157
162
|
|
|
158
163
|
# Display status messages and management link
|
|
159
|
-
_display_status_message(plan_credits, purchased_credits)
|
|
164
|
+
_display_status_message(plan_credits, purchased_credits, promo_credits)
|
|
160
165
|
console.print("\nTo manage your plan navigate to https://platform.codeplain.ai/plans")
|
{codeplain-0.3.2 → codeplain-0.3.4}/examples/example_hello_world_python/hello_world_python.plain
RENAMED
|
@@ -13,7 +13,7 @@ import:
|
|
|
13
13
|
- Display "hello, world"
|
|
14
14
|
|
|
15
15
|
***acceptance tests***
|
|
16
|
+
|
|
17
|
+
- :App: should exit with status code 0 indicating successful execution.
|
|
16
18
|
|
|
17
|
-
- :App: should
|
|
18
|
-
|
|
19
|
-
- :App: should complete execution in under 1 second.
|
|
19
|
+
- :App: should complete execution in under 1 second.
|
|
@@ -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)}
|