codeplain 0.3.8__py3-none-any.whl → 0.3.10.dev1__py3-none-any.whl
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.8.dist-info → codeplain-0.3.10.dev1.dist-info}/METADATA +36 -8
- {codeplain-0.3.8.dist-info → codeplain-0.3.10.dev1.dist-info}/RECORD +38 -34
- {codeplain-0.3.8.dist-info → codeplain-0.3.10.dev1.dist-info}/WHEEL +1 -1
- concept_utils.py +6 -0
- git_utils.py +0 -10
- memory_management.py +2 -3
- metadata_utils.py +46 -0
- module_renderer.py +9 -6
- partial_rendering.py +35 -4
- plain2code.py +37 -4
- plain2code_arguments.py +13 -19
- plain2code_console.py +3 -1
- plain2code_exceptions.py +8 -0
- plain2code_logger.py +37 -15
- plain2code_telemetry.py +17 -11
- plain_file.py +18 -1
- plain_modules.py +264 -42
- render_machine/actions/prepare_repositories.py +8 -10
- render_machine/conformance_tests.py +48 -9
- render_machine/implementation_code_helpers.py +5 -1
- render_machine/render_context.py +14 -4
- render_machine/render_utils.py +1 -2
- system_config.py +80 -0
- tests/e2e/test_hello_world_python.py +1 -1
- tests/e2e/test_hello_world_python_windows.py +1 -1
- tests/test_arg_provenance.py +3 -3
- tests/test_cli_output.py +13 -0
- tests/test_folder_path_resolution.py +12 -13
- tests/test_headless_logging.py +177 -0
- tests/test_plain2code.py +1 -2
- tests/test_plain_modules.py +467 -16
- tests/test_plainfileparser.py +41 -4
- tests/test_prepare_repositories_layout.py +277 -0
- tests/test_system_config.py +95 -0
- tests/test_telemetry.py +31 -10
- tui/plain_module_render_choice_tui.py +25 -8
- {codeplain-0.3.8.dist-info → codeplain-0.3.10.dev1.dist-info}/entry_points.txt +0 -0
- {codeplain-0.3.8.dist-info → codeplain-0.3.10.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: codeplain
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10.dev1
|
|
4
4
|
Summary: Transform plain language specifications into working code
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -8,15 +8,15 @@ Classifier: Intended Audience :: Developers
|
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Topic :: Software Development :: Code Generators
|
|
10
10
|
Requires-Python: ==3.11.*
|
|
11
|
-
Requires-Dist: gitpython==3.1.
|
|
12
|
-
Requires-Dist: mistletoe==1.
|
|
11
|
+
Requires-Dist: gitpython==3.1.55
|
|
12
|
+
Requires-Dist: mistletoe==1.6.0
|
|
13
13
|
Requires-Dist: networkx==3.6.1
|
|
14
14
|
Requires-Dist: python-frontmatter==1.3.0
|
|
15
15
|
Requires-Dist: python-liquid2==0.4.0
|
|
16
16
|
Requires-Dist: pyyaml==6.0.3
|
|
17
17
|
Requires-Dist: requests==2.34.2
|
|
18
18
|
Requires-Dist: rich==15.0.0
|
|
19
|
-
Requires-Dist: sentry-sdk==2.66.
|
|
19
|
+
Requires-Dist: sentry-sdk==2.66.1
|
|
20
20
|
Requires-Dist: textual>=7.5.0
|
|
21
21
|
Requires-Dist: tiktoken==0.13.0
|
|
22
22
|
Requires-Dist: transitions==0.9.3
|
|
@@ -52,6 +52,7 @@ macOS and Linux:
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
curl -fsSL https://codeplain.ai/install.sh | bash
|
|
55
|
+
# re-run to update to the latest version
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
Windows PowerShell:
|
|
@@ -74,12 +75,39 @@ export CODEPLAIN_API_KEY="your_actual_api_key_here"
|
|
|
74
75
|
|
|
75
76
|
You describe what you want in plain English, an agent helps you write a spec in \*\*\*plain language, and \*codeplain renders the actual code. This is what [plain-forge](https://github.com/Codeplain-ai/plain-forge) sets up: a toolkit of skills, rules, and docs that plugs into your AI coding agent of choice — Claude Code, Codex, ForgeCode, OpenCode, or any other agent that reads from a standard skills directory — and turns a conversation into a complete `.plain` spec, then keeps maintaining it across the lifetime of the project.
|
|
76
77
|
|
|
77
|
-
If you didn't install it during step 1, run it any time:
|
|
78
|
+
If you didn't install it during step 1, run it any time using your favorite Node package runner:
|
|
78
79
|
|
|
79
80
|
```bash
|
|
80
81
|
npx plain-forge install
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
The same command works with any other runner — pick whichever matches your toolchain:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# npm (bundled with Node.js)
|
|
88
|
+
npx plain-forge install
|
|
89
|
+
|
|
90
|
+
# pnpm
|
|
91
|
+
pnpm dlx plain-forge install
|
|
92
|
+
|
|
93
|
+
# Bun
|
|
94
|
+
bunx plain-forge install
|
|
95
|
+
|
|
96
|
+
# Yarn (Berry, v2+)
|
|
97
|
+
yarn dlx plain-forge install
|
|
98
|
+
|
|
99
|
+
# Deno (npm: specifier, needs permissions to write the skills directory)
|
|
100
|
+
deno run -A npm:plain-forge install
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Every runner downloads `plain-forge` on demand and runs it without adding it to your project's dependencies. The package exposes a `plain-forge` binary, so no runner needs an extra `--package` flag.
|
|
104
|
+
|
|
105
|
+
`install` fails if plain-forge is already installed at the target — to refresh an existing installation to the latest skills, rules, and docs, use `update` instead (swap in your runner of choice):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx plain-forge update
|
|
109
|
+
```
|
|
110
|
+
|
|
83
111
|
### 3. Convert specs into tested and validated code
|
|
84
112
|
|
|
85
113
|
\*codeplain extends coding agents with agentic skills for working at the specification layer, and with tools that convert specifications into tested and validated implementation code.
|
|
@@ -101,9 +129,9 @@ You can immediately test the system with a simple "Hello World" example:
|
|
|
101
129
|
|
|
102
130
|
_Note: Rendering will take a few minutes to complete._
|
|
103
131
|
|
|
104
|
-
- The system will generate a Python application in the `
|
|
132
|
+
- The system will generate a Python application in the `dist` directory. You can run it with:
|
|
105
133
|
```bash
|
|
106
|
-
cd
|
|
134
|
+
cd dist
|
|
107
135
|
python hello_world.py
|
|
108
136
|
```
|
|
109
137
|
|
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
change_detection.py,sha256=CazORZrezeZSw2I6VBvieN-c2SEjieZPo6pNrKwmMVA,6915
|
|
2
2
|
codeplain_REST_api.py,sha256=x-sCbbBSswID4eKXDMvv84nzWynNSbBPoyAtq__6tas,21644
|
|
3
|
-
concept_utils.py,sha256=
|
|
3
|
+
concept_utils.py,sha256=TpyiQ4DfMtO231j-lPj65i0suRTPr8t9j3QNf9KiM4A,8080
|
|
4
4
|
diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
|
|
5
5
|
event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
|
|
6
6
|
file_utils.py,sha256=MNZ5li1ofIMOYB6FCrMa_UkX6nkGCgSZuaqzhqVc1Ok,11682
|
|
7
|
-
git_utils.py,sha256=
|
|
8
|
-
memory_management.py,sha256=
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
git_utils.py,sha256=gCUA6_9zg19f6k_0aVcRnIVlAJSBD0Vt6takx-0pKME,16547
|
|
8
|
+
memory_management.py,sha256=SPoK0BXB9_yDyrrOKjFtOOlKkJIq0I5EXV7dE0OsE8g,5972
|
|
9
|
+
metadata_utils.py,sha256=YO17o7IAma5TT1ZN0pTwrLVr_KxFQ8APv02KqKPpI5k,1641
|
|
10
|
+
module_renderer.py,sha256=IJBle8mbBARn_xrekjSpsVID7719yy2rNPkM15-ONoI,6982
|
|
11
|
+
partial_rendering.py,sha256=uzZc2j4d3pAXzfiHw_y1TK4yLRsLOb1Mm21570AGxJ8,13837
|
|
11
12
|
path_resolution.py,sha256=0tMqU0a1g4DYhTOp7QjLVSEkv_cgasb4NI1mK809dUM,2233
|
|
12
|
-
plain2code.py,sha256=
|
|
13
|
-
plain2code_arguments.py,sha256=
|
|
14
|
-
plain2code_console.py,sha256=
|
|
13
|
+
plain2code.py,sha256=NsZf7JS2bGmOQ6HsbiTqNA3EMXXY5YQW6nBBXCG7m-U,17392
|
|
14
|
+
plain2code_arguments.py,sha256=xMK-R13Vu005bHz3MUDY8F0Cf3ED9hCPqNsC6jGZmkk,17620
|
|
15
|
+
plain2code_console.py,sha256=5N4_GIV8qE-2zvFh6qoOHhhsuwCF_3r7jG19S-YWtfk,5906
|
|
15
16
|
plain2code_events.py,sha256=rgDF5PDtNr_lEPvJmd1tIQXZrzwgzZW5b-_pykugqe4,1587
|
|
16
|
-
plain2code_exceptions.py,sha256=
|
|
17
|
-
plain2code_logger.py,sha256=
|
|
17
|
+
plain2code_exceptions.py,sha256=rtFEQbs1ruzSlGXIGJZoZry9sjYzmhPKHiUQIECcbL0,2567
|
|
18
|
+
plain2code_logger.py,sha256=YQ-ENYBHKXROVv6Q52xh7eXrFkj_3Lex__N9zfxlw9Y,5729
|
|
18
19
|
plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
|
|
19
20
|
plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,2235
|
|
20
21
|
plain2code_state.py,sha256=uePQlC29Pbhm1yXfdCFebONYdy6jo13gTTmXSwC11cQ,3168
|
|
21
|
-
plain2code_telemetry.py,sha256=
|
|
22
|
+
plain2code_telemetry.py,sha256=e4UCAoFklJMDWGQsL50i7nTLiOBy8gs86A0XO4WLgPo,4597
|
|
22
23
|
plain2code_utils.py,sha256=unOxuqSQRpwG8YF6uoQhnZ3ZSTJRIUCPLdJ-1lRZR5M,1534
|
|
23
|
-
plain_file.py,sha256=
|
|
24
|
-
plain_modules.py,sha256=
|
|
24
|
+
plain_file.py,sha256=Kn8m_w6c04_RR-FBjAU05UcLOTDk4zcctpJXghYdZ8I,35757
|
|
25
|
+
plain_modules.py,sha256=kUGR2mX908FEKOO8b2CJuk2rn9iRj8qyWGFMWTdfzoY,25977
|
|
25
26
|
plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
|
|
26
|
-
system_config.py,sha256=
|
|
27
|
+
system_config.py,sha256=sA8wiPmBMtRsLwSQ40U5pVdLazAafbV-1Z19Yp0HiSQ,5314
|
|
27
28
|
usage_summary.py,sha256=MA4WcVaAwz6IWCN4k_rgxVcwO4I157wN7BZlFi3h0R4,1282
|
|
28
29
|
cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
|
|
29
30
|
cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
|
|
@@ -35,11 +36,11 @@ docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
|
|
|
35
36
|
examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py,sha256=dwTowrHiVKKbrDv21v8xJC30Q57AXZkQasdGOO5JsBE,470
|
|
36
37
|
render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
38
|
render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt1M,3959
|
|
38
|
-
render_machine/conformance_tests.py,sha256=
|
|
39
|
-
render_machine/implementation_code_helpers.py,sha256=
|
|
40
|
-
render_machine/render_context.py,sha256=
|
|
39
|
+
render_machine/conformance_tests.py,sha256=PNTCnJNd_xNIhpTPPS8vlx8qws3cTgQSTLdADRab2YM,9617
|
|
40
|
+
render_machine/implementation_code_helpers.py,sha256=Fxm-IInoZjAIZn41_-WXWj7VIYczpW4yH44Gt8CYMAQ,2378
|
|
41
|
+
render_machine/render_context.py,sha256=iaJWI_EMSn51LNF_b7KRvlir46ZgUkjbaEiVgSvMTCI,26314
|
|
41
42
|
render_machine/render_types.py,sha256=cT3D0lmxQ3enIo0jGp3LEGCY2t3X1Q1_tqwcadzSAv0,8761
|
|
42
|
-
render_machine/render_utils.py,sha256=
|
|
43
|
+
render_machine/render_utils.py,sha256=yA3X15E_Lebk_-MxkSz9rM_qy-Pes8EQ9Ye-smkHCDw,9320
|
|
43
44
|
render_machine/state_machine_config.py,sha256=NnX5sBGkc6cNG8OOQ4PrJeJBCADRGZKJYQXV7ndR4bs,26789
|
|
44
45
|
render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
|
|
45
46
|
render_machine/triggers.py,sha256=VekNhHGR-u5JXIboqc_Eyqf73LNH7wezLK7r3WWtodk,1269
|
|
@@ -52,7 +53,7 @@ render_machine/actions/exit_with_error.py,sha256=W6TMULHJcHEaXE4WUnlELEu0_l7IZy9
|
|
|
52
53
|
render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
|
|
53
54
|
render_machine/actions/fix_conformance_test.py,sha256=EZLVjF4A8zSTmyqdgcLoz-2pLpeieq4M_Rb71pY3IKA,10095
|
|
54
55
|
render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
|
|
55
|
-
render_machine/actions/prepare_repositories.py,sha256=
|
|
56
|
+
render_machine/actions/prepare_repositories.py,sha256=8poItxPrUZWKCFReqdAKL__bxjM71Ig2zLkg1TDGmpY,3439
|
|
56
57
|
render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
|
|
57
58
|
render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
|
|
58
59
|
render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
|
|
@@ -66,45 +67,48 @@ standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOz
|
|
|
66
67
|
standard_template_library/typescript-react-app-template.plain,sha256=DN5wEJcT8ZqQTf0vFKi9Z1isi9cin2UVNOZb3r28d3M,715
|
|
67
68
|
tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
|
|
68
69
|
tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
|
|
69
|
-
tests/test_arg_provenance.py,sha256=
|
|
70
|
+
tests/test_arg_provenance.py,sha256=d_J7aCeHMXBpctBWTt60WU-I8pEKN7XRXRTJZZGCdo0,3441
|
|
70
71
|
tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
|
|
71
|
-
tests/test_cli_output.py,sha256=
|
|
72
|
+
tests/test_cli_output.py,sha256=ZV2F1NNW4EiZvMNkhF9XUZH6bFJbddfkZof5qAFxe3Y,18885
|
|
72
73
|
tests/test_console_log_styles.py,sha256=63DQWiwWNhe_rMP70bW9DAB6-EPaqU3Wpwyh8eR09pc,4080
|
|
73
74
|
tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
|
|
74
|
-
tests/test_folder_path_resolution.py,sha256=
|
|
75
|
+
tests/test_folder_path_resolution.py,sha256=K9v684xkPE0HOAsovg8uDixpZmaIk7ho-Wd3rTjKLF0,6560
|
|
75
76
|
tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
|
|
77
|
+
tests/test_headless_logging.py,sha256=soNESeotKQaU-SQyxtjpq35l-dLVsauZ_lJ-wk9i2mI,6419
|
|
76
78
|
tests/test_imports.py,sha256=1ncbqQhtQW237kIjyV-Ufl7-R-PuduvoYFj2iS-DXW4,2608
|
|
77
79
|
tests/test_log_file_name_resolution.py,sha256=99g9NgPF-BZ98CvNyqDcltBAx4sLBE8gpQPAAieKiPc,3619
|
|
78
80
|
tests/test_logging_config_path_resolution.py,sha256=avz_ehRd18INwIXvAy1AwVzNNUz0vS8eu1nRe0bra8g,2650
|
|
79
81
|
tests/test_partial_rendering.py,sha256=r0RUUd1pI4-2ZkRrM1j8_kUqv-70hrOGjuKBhZoRmH0,22758
|
|
80
82
|
tests/test_path_resolution.py,sha256=yQpd-Unq1lwNv0wkRPwVs5pDT8JmyyDgbpiTQsyt--Y,2615
|
|
81
|
-
tests/test_plain2code.py,sha256=
|
|
83
|
+
tests/test_plain2code.py,sha256=lyb7iYK8XHu1dmTFh9Q31YgX0faNlCHiyI5M35VQwOg,3780
|
|
82
84
|
tests/test_plain2code_state.py,sha256=cowPZS9oRbv9J49CE4P3Rc47Q5oQOby4bTf2OlnRlBI,1217
|
|
83
85
|
tests/test_plain2code_utils.py,sha256=-W8a7FquMJQ_pTdBoBF1XprzNL2G555QhdtXCy6BDyQ,1237
|
|
84
|
-
tests/test_plain_modules.py,sha256=
|
|
86
|
+
tests/test_plain_modules.py,sha256=Fn3FeH-M4exnyFJDZzc9YWaLEfBmiqPH5TpiBT9jgAs,33033
|
|
85
87
|
tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
|
|
86
|
-
tests/test_plainfileparser.py,sha256=
|
|
88
|
+
tests/test_plainfileparser.py,sha256=l8GfXkVaSzgH74fX45Wp5jX0q3QlO6t_TgDG72dc28o,23867
|
|
87
89
|
tests/test_plainspec.py,sha256=ng55E8ajrH2-gPC1p_QDIe_zXplPvoqHJ-d57UWKFTw,3577
|
|
90
|
+
tests/test_prepare_repositories_layout.py,sha256=iHDfZi2OFLDp4SyjR9o11imKKiTGzycb_1nWBkW7QFE,12515
|
|
88
91
|
tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
|
|
89
92
|
tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
|
|
90
93
|
tests/test_script_path_resolution.py,sha256=tiEqMevQRXr9YpfHN0xldOOQpLAUgnSqTzeZWkKHaUY,4156
|
|
91
|
-
tests/
|
|
94
|
+
tests/test_system_config.py,sha256=gH5qs9q4lh5CljAdz28HUrJzuKqCvkZherFuv7enMak,2787
|
|
95
|
+
tests/test_telemetry.py,sha256=LINvx1_Oz1OJ3ho2RcIcAA29IsNfUbkVVCbFaQcrBaA,8133
|
|
92
96
|
tests/test_tui_usage.py,sha256=sfiAGTpRrT3MRr0-8DH3qjuECSxjODVgwzrK_K3iPXs,7959
|
|
93
97
|
tests/test_usage_summary.py,sha256=Ps0kVzvmdZYeXrBR-cqkn3szTV5neYKs1PmmURngeIc,1079
|
|
94
98
|
tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
|
|
95
|
-
tests/e2e/test_hello_world_python.py,sha256=
|
|
96
|
-
tests/e2e/test_hello_world_python_windows.py,sha256=
|
|
99
|
+
tests/e2e/test_hello_world_python.py,sha256=2Cgs3-79dtQh5gVab257tFDHXTznefZeYqbTjlM4Nng,1632
|
|
100
|
+
tests/e2e/test_hello_world_python_windows.py,sha256=ggynH_Wm4011M500zoPNNgvD-s4vZ3hvlorf1yARgQQ,2019
|
|
97
101
|
tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
|
|
98
102
|
tui/components.py,sha256=XkC4Aq6VyTnFnBQs8PEHwVdAOczhm0ebKN92kiYMbq0,25791
|
|
99
103
|
tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
|
|
100
104
|
tui/plain2code_tui.py,sha256=8F7ZYRpmTiWRdTeQ63LPf2LGrreYjIYXvf_9XNSPyHs,17104
|
|
101
|
-
tui/plain_module_render_choice_tui.py,sha256=
|
|
105
|
+
tui/plain_module_render_choice_tui.py,sha256=XwBMMzyDat9FYqMkKdIV7kmIevddfwGA5Qk9kgy5sYY,12112
|
|
102
106
|
tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
|
|
103
107
|
tui/state_handlers.py,sha256=4r_6m5YMcprQtgiTP0De3XFfGAF8VzgaWXHxPweYPbQ,16325
|
|
104
108
|
tui/styles.css,sha256=kiC7Og_2G1E7owoiZgV95-3_dSeQnsNj3qzaY04xkDg,6727
|
|
105
109
|
tui/widget_helpers.py,sha256=ToO9MV2bF2Rp-XMNcjIYclnB_qmgO5sLw4UtQ2CyTMg,6652
|
|
106
|
-
codeplain-0.3.
|
|
107
|
-
codeplain-0.3.
|
|
108
|
-
codeplain-0.3.
|
|
109
|
-
codeplain-0.3.
|
|
110
|
-
codeplain-0.3.
|
|
110
|
+
codeplain-0.3.10.dev1.dist-info/METADATA,sha256=A3wmebGioQKuw8rISq9tkR7BaQaexRrAnvoikn6L2Oc,7256
|
|
111
|
+
codeplain-0.3.10.dev1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
112
|
+
codeplain-0.3.10.dev1.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
|
|
113
|
+
codeplain-0.3.10.dev1.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
|
|
114
|
+
codeplain-0.3.10.dev1.dist-info/RECORD,,
|
concept_utils.py
CHANGED
|
@@ -12,8 +12,14 @@ from plain2code_exceptions import PlainSyntaxError
|
|
|
12
12
|
DEFAULT_CONCEPTS = {
|
|
13
13
|
":ConformanceTests:",
|
|
14
14
|
":UnitTests:",
|
|
15
|
+
":AcceptanceTest:",
|
|
15
16
|
":AcceptanceTests:",
|
|
16
17
|
":Implementation:",
|
|
18
|
+
":plainDefinitions:",
|
|
19
|
+
":plainImplementationReqs:",
|
|
20
|
+
":plainFunctionality:",
|
|
21
|
+
":plainTestReqs:",
|
|
22
|
+
":plainImplementationCode:",
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
|
git_utils.py
CHANGED
|
@@ -64,7 +64,6 @@ def init_git_repo(
|
|
|
64
64
|
path_to_repo: Union[str, os.PathLike],
|
|
65
65
|
module_name: Optional[str] = None,
|
|
66
66
|
render_id: Optional[str] = None,
|
|
67
|
-
initial_files: Optional[dict[str, str]] = None,
|
|
68
67
|
) -> Repo:
|
|
69
68
|
"""
|
|
70
69
|
Initializes a new git repository in the given path.
|
|
@@ -79,10 +78,6 @@ def init_git_repo(
|
|
|
79
78
|
repo = Repo.init(path_to_repo)
|
|
80
79
|
_ensure_git_config(repo)
|
|
81
80
|
|
|
82
|
-
if initial_files:
|
|
83
|
-
file_utils.store_response_files(path_to_repo, initial_files, [])
|
|
84
|
-
repo.git.add(".")
|
|
85
|
-
|
|
86
81
|
repo.git.commit(
|
|
87
82
|
"--allow-empty", "-m", _get_full_commit_message(INITIAL_COMMIT_MESSAGE, module_name, None, render_id)
|
|
88
83
|
)
|
|
@@ -95,14 +90,9 @@ def clone_repo(
|
|
|
95
90
|
new_repo_path: str,
|
|
96
91
|
module_name: Optional[str] = None,
|
|
97
92
|
render_id: Optional[str] = None,
|
|
98
|
-
initial_files: Optional[dict[str, str]] = None,
|
|
99
93
|
) -> Repo:
|
|
100
94
|
repo = Repo.clone_from(source_repo_path, new_repo_path)
|
|
101
95
|
|
|
102
|
-
if initial_files:
|
|
103
|
-
file_utils.store_response_files(new_repo_path, initial_files, [])
|
|
104
|
-
repo.git.add(".")
|
|
105
|
-
|
|
106
96
|
repo.git.commit(
|
|
107
97
|
"--allow-empty", "-m", _get_full_commit_message(INITIAL_COMMIT_MESSAGE, module_name, None, render_id)
|
|
108
98
|
)
|
memory_management.py
CHANGED
|
@@ -3,7 +3,6 @@ import os
|
|
|
3
3
|
|
|
4
4
|
import file_utils
|
|
5
5
|
from plain2code_console import console
|
|
6
|
-
from plain_modules import CODEPLAIN_MEMORY_SUBFOLDER
|
|
7
6
|
from render_machine.implementation_code_helpers import ImplementationCodeHelpers
|
|
8
7
|
from render_machine.render_context import RenderContext
|
|
9
8
|
|
|
@@ -24,9 +23,9 @@ class MemoryManager:
|
|
|
24
23
|
console.debug(f"Loaded {len(memory_files_content)} memory files.")
|
|
25
24
|
return memory_files, memory_files_content
|
|
26
25
|
|
|
27
|
-
def __init__(self, codeplain_api,
|
|
26
|
+
def __init__(self, codeplain_api, memory_folder: str):
|
|
28
27
|
self.codeplain_api = codeplain_api
|
|
29
|
-
self.memory_folder =
|
|
28
|
+
self.memory_folder = memory_folder
|
|
30
29
|
|
|
31
30
|
def create_conformance_tests_memory(
|
|
32
31
|
self, render_context: RenderContext, exit_code: int, conformance_tests_issue: str
|
metadata_utils.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Read/write helpers for the per-module metadata file (``module_metadata.json``).
|
|
2
|
+
|
|
3
|
+
These helpers operate purely on a metadata path or a plain dict. Module-specific
|
|
4
|
+
logic — computing the hashes, assembling the payload from the spec and required
|
|
5
|
+
modules — stays on :class:`plain_modules.PlainModule`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
MODULE_METADATA_FILENAME = "module_metadata.json"
|
|
14
|
+
MODULE_FUNCTIONALITIES = "functionalities"
|
|
15
|
+
REQUIRED_MODULES_FUNCTIONALITIES = "required_modules_functionalities"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_metadata(metadata_path: str) -> dict | None:
|
|
19
|
+
"""Return the parsed metadata dict, or None if the file does not exist."""
|
|
20
|
+
if not os.path.exists(metadata_path):
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
with open(metadata_path, "r", encoding="utf-8") as f:
|
|
24
|
+
return json.load(f)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def write_metadata(metadata_path: str, metadata: dict) -> None:
|
|
28
|
+
"""Write metadata as indented JSON, creating the parent folder if needed."""
|
|
29
|
+
os.makedirs(os.path.dirname(metadata_path), exist_ok=True)
|
|
30
|
+
|
|
31
|
+
with open(metadata_path, "w", encoding="utf-8") as f:
|
|
32
|
+
json.dump(metadata, f, indent=4)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def truncate_functionalities(metadata: dict, keep_count: int) -> bool:
|
|
36
|
+
"""Trim the functionalities list in ``metadata`` in place to ``keep_count`` entries.
|
|
37
|
+
|
|
38
|
+
Returns True if the list was shortened, False if it was already short enough
|
|
39
|
+
(in which case ``metadata`` is left untouched).
|
|
40
|
+
"""
|
|
41
|
+
functionalities = metadata.get(MODULE_FUNCTIONALITIES, [])
|
|
42
|
+
if len(functionalities) <= keep_count:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
metadata[MODULE_FUNCTIONALITIES] = functionalities[:keep_count]
|
|
46
|
+
return True
|
module_renderer.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import os
|
|
3
2
|
import threading
|
|
4
3
|
|
|
5
4
|
from event_bus import EventBus
|
|
@@ -48,9 +47,8 @@ class ModuleRenderer:
|
|
|
48
47
|
self.codeplainAPI,
|
|
49
48
|
memory_manager,
|
|
50
49
|
plain_module,
|
|
51
|
-
build_folder=
|
|
50
|
+
build_folder=plain_module.module_build_folder,
|
|
52
51
|
build_dest=self.args.build_dest,
|
|
53
|
-
conformance_tests_folder=self.args.conformance_tests_folder,
|
|
54
52
|
conformance_tests_dest=self.args.conformance_tests_dest,
|
|
55
53
|
unittests_script=self.args.unittests_script,
|
|
56
54
|
conformance_tests_script=self.args.conformance_tests_script,
|
|
@@ -113,10 +111,15 @@ class ModuleRenderer:
|
|
|
113
111
|
):
|
|
114
112
|
return False, False
|
|
115
113
|
|
|
114
|
+
# We are going to (re)render this module. If it currently exists only as a
|
|
115
|
+
# "<module>.module" archive, unpack it into the real plain_modules/<module>/ folder now,
|
|
116
|
+
# before RenderContext snapshots the build folder path and before the git repos, metadata,
|
|
117
|
+
# and memory folder are touched.
|
|
118
|
+
plain_module.ensure_module_unpacked()
|
|
119
|
+
|
|
116
120
|
memory_manager = MemoryManager(
|
|
117
121
|
self.codeplainAPI,
|
|
118
|
-
plain_module.
|
|
119
|
-
self.args.conformance_tests_folder,
|
|
122
|
+
plain_module.module_memory_folder,
|
|
120
123
|
)
|
|
121
124
|
render_context = self._build_render_context_for_module(
|
|
122
125
|
plain_module,
|
|
@@ -165,7 +168,7 @@ class ModuleRenderer:
|
|
|
165
168
|
if self.args.copy_build:
|
|
166
169
|
rendered_code_path = f"{self.args.build_dest}/"
|
|
167
170
|
else:
|
|
168
|
-
rendered_code_path = self.
|
|
171
|
+
rendered_code_path = self.plain_module.module_build_folder
|
|
169
172
|
|
|
170
173
|
self.run_state.set_render_generated_code_path(rendered_code_path)
|
|
171
174
|
self.event_bus.publish(RenderCompleted(rendered_code_path=rendered_code_path))
|
partial_rendering.py
CHANGED
|
@@ -12,7 +12,7 @@ class PlainModuleRenderState:
|
|
|
12
12
|
last_render_module: PlainModule
|
|
13
13
|
last_render_frid: str | None
|
|
14
14
|
change: PlainModule | None = None
|
|
15
|
-
change_type: Literal["spec_change", "code_change"] | None = None
|
|
15
|
+
change_type: Literal["spec_change", "code_change", "missing_conformance_tests"] | None = None
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@dataclass
|
|
@@ -56,6 +56,19 @@ def code_change(plain_module: PlainModule) -> PlainModule | None:
|
|
|
56
56
|
return None
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def archive_missing_conformance_tests(plain_module: PlainModule, render_conformance_tests: bool) -> PlainModule | None:
|
|
60
|
+
"""Return the first module available only as a "<module>.module" archive that has no conformance
|
|
61
|
+
tests while conformance testing is enabled. Such an archive cannot be consumed as-is (regression
|
|
62
|
+
needs the tests), so the module must be re-rendered."""
|
|
63
|
+
if not render_conformance_tests:
|
|
64
|
+
return None
|
|
65
|
+
all_modules = plain_module.all_required_modules + [plain_module]
|
|
66
|
+
for _module in all_modules:
|
|
67
|
+
if _module.is_archived_only() and not _module.archive_has_conformance_tests():
|
|
68
|
+
return _module
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
59
72
|
def module_comes_before_or_equal(
|
|
60
73
|
all_required_modules: list[PlainModule],
|
|
61
74
|
module1: PlainModule,
|
|
@@ -70,15 +83,24 @@ def module_comes_before_or_equal(
|
|
|
70
83
|
raise ValueError(f"Module {module1.module_name} and {module2.module_name} not found in {all_required_modules}")
|
|
71
84
|
|
|
72
85
|
|
|
73
|
-
def get_plain_module_render_state(
|
|
86
|
+
def get_plain_module_render_state(
|
|
87
|
+
plain_module: PlainModule, render_conformance_tests: bool = False
|
|
88
|
+
) -> PlainModuleRenderState | None:
|
|
74
89
|
sc = spec_change(plain_module)
|
|
75
90
|
cc = code_change(plain_module)
|
|
91
|
+
mt = archive_missing_conformance_tests(plain_module, render_conformance_tests)
|
|
76
92
|
all_required_modules = plain_module.all_required_modules
|
|
77
93
|
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:
|
|
94
|
+
if last_rendered_module_name is None and last_rendered_frid is None and mt is None:
|
|
79
95
|
return None
|
|
80
96
|
|
|
81
|
-
if last_rendered_module_name
|
|
97
|
+
if last_rendered_module_name is None:
|
|
98
|
+
# Nothing has been rendered yet, but an archived module blocks consumption (no tests).
|
|
99
|
+
# Anchor the state on that module so the user is prompted to re-render it.
|
|
100
|
+
assert mt is not None # guaranteed by the early return above
|
|
101
|
+
module = mt
|
|
102
|
+
last_rendered_frid = None
|
|
103
|
+
elif last_rendered_module_name == plain_module.module_name:
|
|
82
104
|
module = plain_module
|
|
83
105
|
else:
|
|
84
106
|
found_module: PlainModule | None = None
|
|
@@ -99,6 +121,13 @@ def get_plain_module_render_state(plain_module: PlainModule) -> PlainModuleRende
|
|
|
99
121
|
change_type=None,
|
|
100
122
|
)
|
|
101
123
|
|
|
124
|
+
# An archive that lacks conformance tests (while testing is enabled) is a hard blocker: it cannot
|
|
125
|
+
# be consumed as-is, so it takes precedence over spec/code changes.
|
|
126
|
+
if mt is not None:
|
|
127
|
+
pr.change = mt
|
|
128
|
+
pr.change_type = "missing_conformance_tests"
|
|
129
|
+
return pr
|
|
130
|
+
|
|
102
131
|
if sc is None and cc is None:
|
|
103
132
|
return pr
|
|
104
133
|
|
|
@@ -129,6 +158,8 @@ def get_all_affected_modules_from_change(
|
|
|
129
158
|
start_module = plain_module.get_next_module(plain_module_render_state.change.module_name)
|
|
130
159
|
else:
|
|
131
160
|
start_module = plain_module_render_state.change
|
|
161
|
+
elif plain_module_render_state.change_type == "missing_conformance_tests":
|
|
162
|
+
start_module = plain_module_render_state.change
|
|
132
163
|
else:
|
|
133
164
|
raise ValueError(f"Unknown change type: {plain_module_render_state.change_type}")
|
|
134
165
|
|
plain2code.py
CHANGED
|
@@ -28,6 +28,7 @@ from plain2code_exceptions import (
|
|
|
28
28
|
ImportedModuleWithFunctionalitiesError,
|
|
29
29
|
InvalidAPIKey,
|
|
30
30
|
InvalidFridArgument,
|
|
31
|
+
InvalidModuleArchiveError,
|
|
31
32
|
MissingAPIKey,
|
|
32
33
|
MissingFunctionalitiesError,
|
|
33
34
|
MissingPreviousFunctionalitiesError,
|
|
@@ -43,11 +44,13 @@ from plain2code_exceptions import (
|
|
|
43
44
|
)
|
|
44
45
|
from plain2code_logger import (
|
|
45
46
|
LOGGER_NAME,
|
|
47
|
+
NARRATION_HANDLER_NAME,
|
|
46
48
|
CrashLogHandler,
|
|
47
49
|
ElapsedTimeFormatter,
|
|
48
50
|
IndentedFormatter,
|
|
49
51
|
LoggingHandler,
|
|
50
52
|
dump_crash_logs,
|
|
53
|
+
stop_narrating,
|
|
51
54
|
)
|
|
52
55
|
from plain2code_state import RunState
|
|
53
56
|
from plain2code_telemetry import capture_crash, initialize_telemetry
|
|
@@ -79,6 +82,7 @@ EXPECTED_EXCEPTIONS = (
|
|
|
79
82
|
UnsupportedResourceType,
|
|
80
83
|
UnsupportedBase64Content,
|
|
81
84
|
GitNotInstalledError,
|
|
85
|
+
InvalidModuleArchiveError,
|
|
82
86
|
SystemExit,
|
|
83
87
|
)
|
|
84
88
|
|
|
@@ -123,6 +127,17 @@ def setup_logging(
|
|
|
123
127
|
handler = LoggingHandler(event_bus, run_state)
|
|
124
128
|
handler.setFormatter(formatter)
|
|
125
129
|
root_logger.addHandler(handler)
|
|
130
|
+
else:
|
|
131
|
+
# Headless has no TUI to narrate the render and suppresses Rich output, so
|
|
132
|
+
# without this the process says nothing on stdout for its entire run: a CI job
|
|
133
|
+
# cannot distinguish a render wedged for hours from a healthy one until the
|
|
134
|
+
# log file is collected at the end. StreamHandler flushes
|
|
135
|
+
# per record, so these arrive as they happen.
|
|
136
|
+
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
137
|
+
stdout_handler.setFormatter(file_formatter)
|
|
138
|
+
stdout_handler.setLevel(configured_log_level)
|
|
139
|
+
stdout_handler.set_name(NARRATION_HANDLER_NAME)
|
|
140
|
+
root_logger.addHandler(stdout_handler)
|
|
126
141
|
|
|
127
142
|
if log_to_file:
|
|
128
143
|
try:
|
|
@@ -212,9 +227,24 @@ def render( # noqa: C901
|
|
|
212
227
|
|
|
213
228
|
warn_if_acceptance_tests_without_conformance_script(plain_module, args)
|
|
214
229
|
|
|
230
|
+
# A built module can be distributed as a "<module>.module" zip archive instead of an unpacked directory.
|
|
231
|
+
# Extract any such module to a scratch location up front. Scratch dirs are removed in main()'s finally.
|
|
232
|
+
# Skip an archive-only module that lacks conformance tests while testing is enabled: it cannot be
|
|
233
|
+
# consumed as-is, so leave it unmaterialized so it is detected below as needing a re-render.
|
|
234
|
+
for module in plain_module.all_required_modules + [plain_module]:
|
|
235
|
+
if args.render_conformance_tests and module.is_archived_only() and not module.archive_has_conformance_tests():
|
|
236
|
+
continue
|
|
237
|
+
module.materialize()
|
|
238
|
+
|
|
239
|
+
# The module_metadata file lives outside the code git repo. That means that a crash mid-render can leave it
|
|
240
|
+
# claiming a functionality was implemented even thought it wasn't yet committed (because of the crash).
|
|
241
|
+
# Out of precaution, this reconciles every module_metadata against the code repo.
|
|
242
|
+
for module in plain_module.all_required_modules + [plain_module]:
|
|
243
|
+
module.reconcile_metadata_with_git()
|
|
244
|
+
|
|
215
245
|
render_choice = None
|
|
216
246
|
if render_range is None:
|
|
217
|
-
plain_module_render_state = get_plain_module_render_state(plain_module)
|
|
247
|
+
plain_module_render_state = get_plain_module_render_state(plain_module, args.render_conformance_tests)
|
|
218
248
|
if plain_module_render_state is not None:
|
|
219
249
|
render_choices = get_render_choices(plain_module, plain_module_render_state, args.force_render)
|
|
220
250
|
ask_user = True
|
|
@@ -328,7 +358,7 @@ def main(): # noqa: C901
|
|
|
328
358
|
sys.exit(1)
|
|
329
359
|
|
|
330
360
|
if not args.api:
|
|
331
|
-
args.api =
|
|
361
|
+
args.api = system_config.default_api_url
|
|
332
362
|
|
|
333
363
|
try:
|
|
334
364
|
print_status(args.api_key, args.api, system_config.client_version)
|
|
@@ -356,7 +386,6 @@ def main(): # noqa: C901
|
|
|
356
386
|
plain_module = plain_modules.PlainModule(
|
|
357
387
|
os.path.basename(args.filename),
|
|
358
388
|
args.build_folder,
|
|
359
|
-
args.conformance_tests_folder,
|
|
360
389
|
template_dirs,
|
|
361
390
|
)
|
|
362
391
|
except Exception as e:
|
|
@@ -373,7 +402,7 @@ def main(): # noqa: C901
|
|
|
373
402
|
event_bus = EventBus()
|
|
374
403
|
|
|
375
404
|
if not args.api:
|
|
376
|
-
args.api =
|
|
405
|
+
args.api = system_config.default_api_url
|
|
377
406
|
|
|
378
407
|
run_state = RunState(spec_filename=args.filename, replay_with=args.replay_with)
|
|
379
408
|
|
|
@@ -407,11 +436,15 @@ def main(): # noqa: C901
|
|
|
407
436
|
if exc_info:
|
|
408
437
|
dump_crash_logs(args, run_state)
|
|
409
438
|
capture_crash(exc_info, run_state, args)
|
|
439
|
+
stop_narrating()
|
|
410
440
|
print_exit_summary(
|
|
411
441
|
run_state,
|
|
412
442
|
args.filename,
|
|
413
443
|
error_message=error_message,
|
|
414
444
|
)
|
|
445
|
+
# Remove any scratch extractions created for archive-only ("<module>.module") modules.
|
|
446
|
+
for module in plain_module.all_required_modules + [plain_module]:
|
|
447
|
+
module.cleanup_scratch()
|
|
415
448
|
|
|
416
449
|
if args.headless and (exc_info is not None or not run_state.render_succeeded):
|
|
417
450
|
sys.exit(1)
|