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.
Files changed (38) hide show
  1. {codeplain-0.3.8.dist-info → codeplain-0.3.10.dev1.dist-info}/METADATA +36 -8
  2. {codeplain-0.3.8.dist-info → codeplain-0.3.10.dev1.dist-info}/RECORD +38 -34
  3. {codeplain-0.3.8.dist-info → codeplain-0.3.10.dev1.dist-info}/WHEEL +1 -1
  4. concept_utils.py +6 -0
  5. git_utils.py +0 -10
  6. memory_management.py +2 -3
  7. metadata_utils.py +46 -0
  8. module_renderer.py +9 -6
  9. partial_rendering.py +35 -4
  10. plain2code.py +37 -4
  11. plain2code_arguments.py +13 -19
  12. plain2code_console.py +3 -1
  13. plain2code_exceptions.py +8 -0
  14. plain2code_logger.py +37 -15
  15. plain2code_telemetry.py +17 -11
  16. plain_file.py +18 -1
  17. plain_modules.py +264 -42
  18. render_machine/actions/prepare_repositories.py +8 -10
  19. render_machine/conformance_tests.py +48 -9
  20. render_machine/implementation_code_helpers.py +5 -1
  21. render_machine/render_context.py +14 -4
  22. render_machine/render_utils.py +1 -2
  23. system_config.py +80 -0
  24. tests/e2e/test_hello_world_python.py +1 -1
  25. tests/e2e/test_hello_world_python_windows.py +1 -1
  26. tests/test_arg_provenance.py +3 -3
  27. tests/test_cli_output.py +13 -0
  28. tests/test_folder_path_resolution.py +12 -13
  29. tests/test_headless_logging.py +177 -0
  30. tests/test_plain2code.py +1 -2
  31. tests/test_plain_modules.py +467 -16
  32. tests/test_plainfileparser.py +41 -4
  33. tests/test_prepare_repositories_layout.py +277 -0
  34. tests/test_system_config.py +95 -0
  35. tests/test_telemetry.py +31 -10
  36. tui/plain_module_render_choice_tui.py +25 -8
  37. {codeplain-0.3.8.dist-info → codeplain-0.3.10.dev1.dist-info}/entry_points.txt +0 -0
  38. {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.4
1
+ Metadata-Version: 2.5
2
2
  Name: codeplain
3
- Version: 0.3.8
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.52
12
- Requires-Dist: mistletoe==1.5.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.0
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 `build` directory. You can run it with:
132
+ - The system will generate a Python application in the `dist` directory. You can run it with:
105
133
  ```bash
106
- cd build
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=D4qVHFBRvfJZwiIq1AD_SV0Ev-RnOMg2yGWo6Cal7PM,7913
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=LQQnL5DAFpH5fDBQgUZX2WfjWY4YC5OEGBPIXryTmnY,16896
8
- memory_management.py,sha256=nHNudgaOqp5UGkIMi62TRCc2nUeFOM0w-I1FfxBFaQE,6120
9
- module_renderer.py,sha256=DZCIo_QzvhFw8EqQTKeF3HUbvfumrV2O1sz-gUhWpIA,6752
10
- partial_rendering.py,sha256=0ChOaGytogBUEGISpbWkbjT9Hqy8pnOkJdJXcitJDgk,12219
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=YLwwzAvE0EWz51V9p8b_kbTltkPvhA2Q8J4JrYyMrAM,15327
13
- plain2code_arguments.py,sha256=3tAA9Haw-j97zztjXdhi6LxFtURP0Tfg9J_rvJDoXmw,17685
14
- plain2code_console.py,sha256=Ifs8tmfNX31UujQ5_0sk9AJcajMG_6qhF-6QZcfbcEE,5773
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=wkkEXw2k4LCd09PIH3_Hu0HyZvtXYjFaA653Sfey4FA,2287
17
- plain2code_logger.py,sha256=NRPLVIMnCHNVl6dII9gFoIzMdr8xa7icnL0T_Fx2XLw,4839
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=7yuTH7mQE_wGVW2H1oiVZ3UJg98ArB5HcUUm9isnv9g,4354
22
+ plain2code_telemetry.py,sha256=e4UCAoFklJMDWGQsL50i7nTLiOBy8gs86A0XO4WLgPo,4597
22
23
  plain2code_utils.py,sha256=unOxuqSQRpwG8YF6uoQhnZ3ZSTJRIUCPLdJ-1lRZR5M,1534
23
- plain_file.py,sha256=k2rcq53q0kOf9_8egrZILpkrVkZdfJt7HU0gHBFskU4,35116
24
- plain_modules.py,sha256=uvRPfxBJwstaWi5lgYKgtQdhhf0Naiy2mS41tLw_TjY,16186
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=VyE6MhXfi6agZvE02p_G4K8yCHkLh3eu890cBzPyDvs,2196
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=MeEveEGJTClLGxWw04P4TR6U9kX9Qgz9qtxNrB9a8QI,7085
39
- render_machine/implementation_code_helpers.py,sha256=LdHNLoKgVtrDaGH2APX8C9vjNjLBUhu2V7OAaJvm4rM,2009
40
- render_machine/render_context.py,sha256=eoaO807LntIRYjLSpc3x7bvwBnFfzLlHmKkyKlIRrxs,25697
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=Jb9NJ73yV1qCfGy9S5A9zcnz0FGVt3MHG8Ofk-9A-aI,9355
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=eE-uU6H_K5IQMraVWSRi4TKk_XPrnA878T_JwtI9cWo,3410
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=Vp-ZtNbTlSNOE8iiHOin3YI9sobbN0tRPUIfrYjxttQ,3459
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=iKS1Li4yhoIL0ezJ58o4oEf9V8TiKcQkvQLVtJpdM4c,18420
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=_ZvVFu5m2-AH8iQNn0fCYkA0flVxVwdh1oQiWcPNIxw,6611
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=DQYFc0JVSkMI8LZCNaQ__2Nph-DcZZW17rprJA54eyw,3851
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=S7sd4ZYNotXOgCyZzmBd714QFLih69nlZ3tV8WNtVPo,13766
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=xd9P0KqZJlJPn3RKT510IQbfBotk6nQ9O8p9iLwJiQk,22815
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/test_telemetry.py,sha256=hjm81aRQQZcZ0RB6aVbS3WO46_nIAii_csmbbtnUh74,7110
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=PQy4elzB7WvpiWLKa6_HFO0K8HA6_6hGLaAqRi0J3OU,1627
96
- tests/e2e/test_hello_world_python_windows.py,sha256=kJQZIuzj8OEEtgBVxnzFvyQJ_wvP2oBu9L-E9geg8sE,2010
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=DRl_xhUbUJsyOaCYt_uNDSLUAA3Ty9NYI9zFrVDwp9Y,11270
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.8.dist-info/METADATA,sha256=nfj-9PV0EN0MS1W9ILf1GyOnSfcB-YSyEXW04UY9vHQ,6351
107
- codeplain-0.3.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
108
- codeplain-0.3.8.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
109
- codeplain-0.3.8.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
110
- codeplain-0.3.8.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
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, module_name: str, conformance_tests_folder: str):
26
+ def __init__(self, codeplain_api, memory_folder: str):
28
27
  self.codeplain_api = codeplain_api
29
- self.memory_folder = os.path.join(conformance_tests_folder, module_name, CODEPLAIN_MEMORY_SUBFOLDER)
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=os.path.join(self.args.build_folder, plain_module.module_name),
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.module_name,
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.args.build_folder
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(plain_module: PlainModule) -> PlainModuleRenderState | None:
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 == plain_module.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 = "https://api.codeplain.ai"
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 = "https://api.codeplain.ai"
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)