codeplain 0.3.9__py3-none-any.whl → 0.3.10.dev2__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.9.dist-info → codeplain-0.3.10.dev2.dist-info}/METADATA +36 -8
- {codeplain-0.3.9.dist-info → codeplain-0.3.10.dev2.dist-info}/RECORD +27 -26
- {codeplain-0.3.9.dist-info → codeplain-0.3.10.dev2.dist-info}/WHEEL +1 -1
- concept_utils.py +6 -0
- module_renderer.py +6 -0
- partial_rendering.py +35 -4
- plain2code.py +31 -3
- plain2code_arguments.py +7 -4
- plain2code_console.py +3 -1
- plain2code_exceptions.py +8 -0
- plain2code_logger.py +37 -15
- plain_file.py +18 -1
- plain_modules.py +197 -7
- render_machine/actions/prepare_repositories.py +5 -0
- render_machine/conformance_tests.py +40 -3
- render_machine/implementation_code_helpers.py +5 -1
- render_machine/render_context.py +13 -1
- system_config.py +35 -0
- tests/test_cli_output.py +13 -0
- tests/test_headless_logging.py +177 -0
- tests/test_plain_modules.py +267 -1
- tests/test_plainfileparser.py +41 -4
- tests/test_prepare_repositories_layout.py +158 -0
- tests/test_system_config.py +37 -0
- tui/plain_module_render_choice_tui.py +25 -8
- {codeplain-0.3.9.dist-info → codeplain-0.3.10.dev2.dist-info}/entry_points.txt +0 -0
- {codeplain-0.3.9.dist-info → codeplain-0.3.10.dev2.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.dev2
|
|
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,30 +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
7
|
git_utils.py,sha256=gCUA6_9zg19f6k_0aVcRnIVlAJSBD0Vt6takx-0pKME,16547
|
|
8
8
|
memory_management.py,sha256=SPoK0BXB9_yDyrrOKjFtOOlKkJIq0I5EXV7dE0OsE8g,5972
|
|
9
9
|
metadata_utils.py,sha256=YO17o7IAma5TT1ZN0pTwrLVr_KxFQ8APv02KqKPpI5k,1641
|
|
10
|
-
module_renderer.py,sha256=
|
|
11
|
-
partial_rendering.py,sha256=
|
|
10
|
+
module_renderer.py,sha256=IJBle8mbBARn_xrekjSpsVID7719yy2rNPkM15-ONoI,6982
|
|
11
|
+
partial_rendering.py,sha256=uzZc2j4d3pAXzfiHw_y1TK4yLRsLOb1Mm21570AGxJ8,13837
|
|
12
12
|
path_resolution.py,sha256=0tMqU0a1g4DYhTOp7QjLVSEkv_cgasb4NI1mK809dUM,2233
|
|
13
|
-
plain2code.py,sha256=
|
|
14
|
-
plain2code_arguments.py,sha256=
|
|
15
|
-
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
|
|
16
16
|
plain2code_events.py,sha256=rgDF5PDtNr_lEPvJmd1tIQXZrzwgzZW5b-_pykugqe4,1587
|
|
17
|
-
plain2code_exceptions.py,sha256=
|
|
18
|
-
plain2code_logger.py,sha256=
|
|
17
|
+
plain2code_exceptions.py,sha256=rtFEQbs1ruzSlGXIGJZoZry9sjYzmhPKHiUQIECcbL0,2567
|
|
18
|
+
plain2code_logger.py,sha256=YQ-ENYBHKXROVv6Q52xh7eXrFkj_3Lex__N9zfxlw9Y,5729
|
|
19
19
|
plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
|
|
20
20
|
plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,2235
|
|
21
21
|
plain2code_state.py,sha256=uePQlC29Pbhm1yXfdCFebONYdy6jo13gTTmXSwC11cQ,3168
|
|
22
22
|
plain2code_telemetry.py,sha256=e4UCAoFklJMDWGQsL50i7nTLiOBy8gs86A0XO4WLgPo,4597
|
|
23
23
|
plain2code_utils.py,sha256=unOxuqSQRpwG8YF6uoQhnZ3ZSTJRIUCPLdJ-1lRZR5M,1534
|
|
24
|
-
plain_file.py,sha256=
|
|
25
|
-
plain_modules.py,sha256=
|
|
24
|
+
plain_file.py,sha256=Kn8m_w6c04_RR-FBjAU05UcLOTDk4zcctpJXghYdZ8I,35757
|
|
25
|
+
plain_modules.py,sha256=kUGR2mX908FEKOO8b2CJuk2rn9iRj8qyWGFMWTdfzoY,25977
|
|
26
26
|
plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
|
|
27
|
-
system_config.py,sha256=
|
|
27
|
+
system_config.py,sha256=sA8wiPmBMtRsLwSQ40U5pVdLazAafbV-1Z19Yp0HiSQ,5314
|
|
28
28
|
usage_summary.py,sha256=MA4WcVaAwz6IWCN4k_rgxVcwO4I157wN7BZlFi3h0R4,1282
|
|
29
29
|
cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
|
|
30
30
|
cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
|
|
@@ -36,9 +36,9 @@ docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
|
|
|
36
36
|
examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py,sha256=dwTowrHiVKKbrDv21v8xJC30Q57AXZkQasdGOO5JsBE,470
|
|
37
37
|
render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
38
|
render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt1M,3959
|
|
39
|
-
render_machine/conformance_tests.py,sha256=
|
|
40
|
-
render_machine/implementation_code_helpers.py,sha256=
|
|
41
|
-
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
|
|
42
42
|
render_machine/render_types.py,sha256=cT3D0lmxQ3enIo0jGp3LEGCY2t3X1Q1_tqwcadzSAv0,8761
|
|
43
43
|
render_machine/render_utils.py,sha256=yA3X15E_Lebk_-MxkSz9rM_qy-Pes8EQ9Ye-smkHCDw,9320
|
|
44
44
|
render_machine/state_machine_config.py,sha256=NnX5sBGkc6cNG8OOQ4PrJeJBCADRGZKJYQXV7ndR4bs,26789
|
|
@@ -53,7 +53,7 @@ render_machine/actions/exit_with_error.py,sha256=W6TMULHJcHEaXE4WUnlELEu0_l7IZy9
|
|
|
53
53
|
render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
|
|
54
54
|
render_machine/actions/fix_conformance_test.py,sha256=EZLVjF4A8zSTmyqdgcLoz-2pLpeieq4M_Rb71pY3IKA,10095
|
|
55
55
|
render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
|
|
56
|
-
render_machine/actions/prepare_repositories.py,sha256=
|
|
56
|
+
render_machine/actions/prepare_repositories.py,sha256=8poItxPrUZWKCFReqdAKL__bxjM71Ig2zLkg1TDGmpY,3439
|
|
57
57
|
render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
|
|
58
58
|
render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
|
|
59
59
|
render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
|
|
@@ -69,11 +69,12 @@ tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
|
|
|
69
69
|
tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
|
|
70
70
|
tests/test_arg_provenance.py,sha256=d_J7aCeHMXBpctBWTt60WU-I8pEKN7XRXRTJZZGCdo0,3441
|
|
71
71
|
tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
|
|
72
|
-
tests/test_cli_output.py,sha256=
|
|
72
|
+
tests/test_cli_output.py,sha256=ZV2F1NNW4EiZvMNkhF9XUZH6bFJbddfkZof5qAFxe3Y,18885
|
|
73
73
|
tests/test_console_log_styles.py,sha256=63DQWiwWNhe_rMP70bW9DAB6-EPaqU3Wpwyh8eR09pc,4080
|
|
74
74
|
tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
|
|
75
75
|
tests/test_folder_path_resolution.py,sha256=K9v684xkPE0HOAsovg8uDixpZmaIk7ho-Wd3rTjKLF0,6560
|
|
76
76
|
tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
|
|
77
|
+
tests/test_headless_logging.py,sha256=soNESeotKQaU-SQyxtjpq35l-dLVsauZ_lJ-wk9i2mI,6419
|
|
77
78
|
tests/test_imports.py,sha256=1ncbqQhtQW237kIjyV-Ufl7-R-PuduvoYFj2iS-DXW4,2608
|
|
78
79
|
tests/test_log_file_name_resolution.py,sha256=99g9NgPF-BZ98CvNyqDcltBAx4sLBE8gpQPAAieKiPc,3619
|
|
79
80
|
tests/test_logging_config_path_resolution.py,sha256=avz_ehRd18INwIXvAy1AwVzNNUz0vS8eu1nRe0bra8g,2650
|
|
@@ -82,15 +83,15 @@ tests/test_path_resolution.py,sha256=yQpd-Unq1lwNv0wkRPwVs5pDT8JmyyDgbpiTQsyt--Y
|
|
|
82
83
|
tests/test_plain2code.py,sha256=lyb7iYK8XHu1dmTFh9Q31YgX0faNlCHiyI5M35VQwOg,3780
|
|
83
84
|
tests/test_plain2code_state.py,sha256=cowPZS9oRbv9J49CE4P3Rc47Q5oQOby4bTf2OlnRlBI,1217
|
|
84
85
|
tests/test_plain2code_utils.py,sha256=-W8a7FquMJQ_pTdBoBF1XprzNL2G555QhdtXCy6BDyQ,1237
|
|
85
|
-
tests/test_plain_modules.py,sha256=
|
|
86
|
+
tests/test_plain_modules.py,sha256=Fn3FeH-M4exnyFJDZzc9YWaLEfBmiqPH5TpiBT9jgAs,33033
|
|
86
87
|
tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
|
|
87
|
-
tests/test_plainfileparser.py,sha256=
|
|
88
|
+
tests/test_plainfileparser.py,sha256=l8GfXkVaSzgH74fX45Wp5jX0q3QlO6t_TgDG72dc28o,23867
|
|
88
89
|
tests/test_plainspec.py,sha256=ng55E8ajrH2-gPC1p_QDIe_zXplPvoqHJ-d57UWKFTw,3577
|
|
89
|
-
tests/test_prepare_repositories_layout.py,sha256=
|
|
90
|
+
tests/test_prepare_repositories_layout.py,sha256=iHDfZi2OFLDp4SyjR9o11imKKiTGzycb_1nWBkW7QFE,12515
|
|
90
91
|
tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
|
|
91
92
|
tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
|
|
92
93
|
tests/test_script_path_resolution.py,sha256=tiEqMevQRXr9YpfHN0xldOOQpLAUgnSqTzeZWkKHaUY,4156
|
|
93
|
-
tests/test_system_config.py,sha256=
|
|
94
|
+
tests/test_system_config.py,sha256=gH5qs9q4lh5CljAdz28HUrJzuKqCvkZherFuv7enMak,2787
|
|
94
95
|
tests/test_telemetry.py,sha256=LINvx1_Oz1OJ3ho2RcIcAA29IsNfUbkVVCbFaQcrBaA,8133
|
|
95
96
|
tests/test_tui_usage.py,sha256=sfiAGTpRrT3MRr0-8DH3qjuECSxjODVgwzrK_K3iPXs,7959
|
|
96
97
|
tests/test_usage_summary.py,sha256=Ps0kVzvmdZYeXrBR-cqkn3szTV5neYKs1PmmURngeIc,1079
|
|
@@ -101,13 +102,13 @@ tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
|
|
|
101
102
|
tui/components.py,sha256=XkC4Aq6VyTnFnBQs8PEHwVdAOczhm0ebKN92kiYMbq0,25791
|
|
102
103
|
tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
|
|
103
104
|
tui/plain2code_tui.py,sha256=8F7ZYRpmTiWRdTeQ63LPf2LGrreYjIYXvf_9XNSPyHs,17104
|
|
104
|
-
tui/plain_module_render_choice_tui.py,sha256=
|
|
105
|
+
tui/plain_module_render_choice_tui.py,sha256=XwBMMzyDat9FYqMkKdIV7kmIevddfwGA5Qk9kgy5sYY,12112
|
|
105
106
|
tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
|
|
106
107
|
tui/state_handlers.py,sha256=4r_6m5YMcprQtgiTP0De3XFfGAF8VzgaWXHxPweYPbQ,16325
|
|
107
108
|
tui/styles.css,sha256=kiC7Og_2G1E7owoiZgV95-3_dSeQnsNj3qzaY04xkDg,6727
|
|
108
109
|
tui/widget_helpers.py,sha256=ToO9MV2bF2Rp-XMNcjIYclnB_qmgO5sLw4UtQ2CyTMg,6652
|
|
109
|
-
codeplain-0.3.
|
|
110
|
-
codeplain-0.3.
|
|
111
|
-
codeplain-0.3.
|
|
112
|
-
codeplain-0.3.
|
|
113
|
-
codeplain-0.3.
|
|
110
|
+
codeplain-0.3.10.dev2.dist-info/METADATA,sha256=mbvu-UgyiC8prG3kCe0j2rUG0CvhfqYFLMEuOrcw1Ao,7256
|
|
111
|
+
codeplain-0.3.10.dev2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
112
|
+
codeplain-0.3.10.dev2.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
|
|
113
|
+
codeplain-0.3.10.dev2.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
|
|
114
|
+
codeplain-0.3.10.dev2.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
|
|
module_renderer.py
CHANGED
|
@@ -111,6 +111,12 @@ class ModuleRenderer:
|
|
|
111
111
|
):
|
|
112
112
|
return False, False
|
|
113
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
|
+
|
|
114
120
|
memory_manager = MemoryManager(
|
|
115
121
|
self.codeplainAPI,
|
|
116
122
|
plain_module.module_memory_folder,
|
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,6 +227,15 @@ 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
|
+
|
|
215
239
|
# The module_metadata file lives outside the code git repo. That means that a crash mid-render can leave it
|
|
216
240
|
# claiming a functionality was implemented even thought it wasn't yet committed (because of the crash).
|
|
217
241
|
# Out of precaution, this reconciles every module_metadata against the code repo.
|
|
@@ -220,7 +244,7 @@ def render( # noqa: C901
|
|
|
220
244
|
|
|
221
245
|
render_choice = None
|
|
222
246
|
if render_range is None:
|
|
223
|
-
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)
|
|
224
248
|
if plain_module_render_state is not None:
|
|
225
249
|
render_choices = get_render_choices(plain_module, plain_module_render_state, args.force_render)
|
|
226
250
|
ask_user = True
|
|
@@ -334,7 +358,7 @@ def main(): # noqa: C901
|
|
|
334
358
|
sys.exit(1)
|
|
335
359
|
|
|
336
360
|
if not args.api:
|
|
337
|
-
args.api =
|
|
361
|
+
args.api = system_config.default_api_url
|
|
338
362
|
|
|
339
363
|
try:
|
|
340
364
|
print_status(args.api_key, args.api, system_config.client_version)
|
|
@@ -378,7 +402,7 @@ def main(): # noqa: C901
|
|
|
378
402
|
event_bus = EventBus()
|
|
379
403
|
|
|
380
404
|
if not args.api:
|
|
381
|
-
args.api =
|
|
405
|
+
args.api = system_config.default_api_url
|
|
382
406
|
|
|
383
407
|
run_state = RunState(spec_filename=args.filename, replay_with=args.replay_with)
|
|
384
408
|
|
|
@@ -412,11 +436,15 @@ def main(): # noqa: C901
|
|
|
412
436
|
if exc_info:
|
|
413
437
|
dump_crash_logs(args, run_state)
|
|
414
438
|
capture_crash(exc_info, run_state, args)
|
|
439
|
+
stop_narrating()
|
|
415
440
|
print_exit_summary(
|
|
416
441
|
run_state,
|
|
417
442
|
args.filename,
|
|
418
443
|
error_message=error_message,
|
|
419
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()
|
|
420
448
|
|
|
421
449
|
if args.headless and (exc_info is not None or not run_state.render_succeeded):
|
|
422
450
|
sys.exit(1)
|
plain2code_arguments.py
CHANGED
|
@@ -6,6 +6,7 @@ from path_resolution import resolve_path
|
|
|
6
6
|
from plain2code_console import console
|
|
7
7
|
from plain2code_exceptions import AmbiguousConfigFileError
|
|
8
8
|
from plain2code_read_config import get_args_from_config
|
|
9
|
+
from system_config import system_config
|
|
9
10
|
|
|
10
11
|
# Attribute on the parsed Namespace mapping each argument dest to its source:
|
|
11
12
|
# "cli" (explicit on the command line), "config" (from config.yaml), or
|
|
@@ -329,8 +330,10 @@ def create_parser():
|
|
|
329
330
|
"--api",
|
|
330
331
|
type=str,
|
|
331
332
|
nargs="?",
|
|
332
|
-
|
|
333
|
-
|
|
333
|
+
# Pre-release builds default to the test API; see
|
|
334
|
+
# system_config._resolve_default_api_url.
|
|
335
|
+
const=system_config.default_api_url,
|
|
336
|
+
help=f"Alternative base URL for the API. Default: `{system_config.default_api_url}`",
|
|
334
337
|
)
|
|
335
338
|
_add_arg(
|
|
336
339
|
parser,
|
|
@@ -424,8 +427,8 @@ def create_parser():
|
|
|
424
427
|
"--headless",
|
|
425
428
|
action="store_true",
|
|
426
429
|
default=False,
|
|
427
|
-
help="Run in headless mode: no TUI
|
|
428
|
-
"
|
|
430
|
+
help="Run in headless mode: no TUI. Progress is written to the log file and streamed "
|
|
431
|
+
"to stdout as it happens; the specification text itself stays in the log file only.",
|
|
429
432
|
)
|
|
430
433
|
|
|
431
434
|
parser.add_argument(
|
plain2code_console.py
CHANGED
|
@@ -67,8 +67,10 @@ class Plain2CodeConsole(Console):
|
|
|
67
67
|
logger.log(level, " ".join(map(str, args)), extra={"log_color": color})
|
|
68
68
|
style = base_style + Style(color=color) if color else base_style
|
|
69
69
|
# Log messages must render exactly as logged: don't interpret square brackets
|
|
70
|
-
# in interpolated content (error texts, file names) as Rich markup
|
|
70
|
+
# in interpolated content (error texts, file names) as Rich markup, and don't
|
|
71
|
+
# let the repr highlighter restyle brackets and numbers inside them.
|
|
71
72
|
kwargs.setdefault("markup", False)
|
|
73
|
+
kwargs.setdefault("highlight", False)
|
|
72
74
|
super().print(*args, **kwargs, style=style)
|
|
73
75
|
|
|
74
76
|
def print_list(self, items, style=None):
|
plain2code_exceptions.py
CHANGED
|
@@ -117,3 +117,11 @@ class GitNotInstalledError(Exception):
|
|
|
117
117
|
"""Raised when git is not installed or not found on PATH."""
|
|
118
118
|
|
|
119
119
|
pass
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class InvalidModuleArchiveError(Exception):
|
|
123
|
+
"""Raised when a ``<module>.module`` archive is missing, corrupt, or has an
|
|
124
|
+
unexpected layout (not a zip, missing ``code/``/``tests/``, ``.git`` not a real
|
|
125
|
+
directory, detached HEAD, or an unsafe member path)."""
|
|
126
|
+
|
|
127
|
+
pass
|
plain2code_logger.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import logging
|
|
2
3
|
|
|
3
4
|
from event_bus import EventBus
|
|
@@ -17,18 +18,28 @@ FILE_LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s"
|
|
|
17
18
|
FILE_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
def _with_indented_message(record, indent: str):
|
|
22
|
+
"""A copy of the record whose continuation lines are indented.
|
|
23
|
+
|
|
24
|
+
One record is handed to every attached handler in turn, so a formatter that
|
|
25
|
+
rewrites `record.msg` in place is rewriting it for the handlers that come after
|
|
26
|
+
it too — a headless render logging to both stdout and a file would indent each
|
|
27
|
+
continuation line twice, once per formatter. Copying keeps each handler's
|
|
28
|
+
formatting local to that handler.
|
|
29
|
+
"""
|
|
30
|
+
indented = copy.copy(record)
|
|
31
|
+
indented.msg = record.getMessage().replace("\n", "\n" + indent)
|
|
32
|
+
indented.args = None # getMessage() already interpolated them into msg
|
|
33
|
+
return indented
|
|
34
|
+
|
|
35
|
+
|
|
20
36
|
class IndentedFormatter(logging.Formatter):
|
|
21
37
|
def __init__(self, fmt=None, datefmt=None, indent=16):
|
|
22
38
|
super().__init__(fmt=fmt, datefmt=datefmt)
|
|
23
39
|
self._indent = " " * indent
|
|
24
40
|
|
|
25
41
|
def format(self, record):
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
modified_message = original_message.replace("\n", "\n" + self._indent)
|
|
29
|
-
|
|
30
|
-
record.msg = modified_message
|
|
31
|
-
return super().format(record)
|
|
42
|
+
return super().format(_with_indented_message(record, self._indent))
|
|
32
43
|
|
|
33
44
|
|
|
34
45
|
class ElapsedTimeFormatter(logging.Formatter):
|
|
@@ -51,16 +62,11 @@ class ElapsedTimeFormatter(logging.Formatter):
|
|
|
51
62
|
seconds = offset_seconds % 60
|
|
52
63
|
elapsed_time = f"[{hours:02d}:{minutes:02d}:{seconds:02d}]"
|
|
53
64
|
|
|
54
|
-
#
|
|
55
|
-
record
|
|
56
|
-
|
|
57
|
-
# Handle multi-line messages with proper indentation
|
|
58
|
-
original_message = record.getMessage()
|
|
59
|
-
indent = " " * len(elapsed_time + " ")
|
|
60
|
-
modified_message = original_message.replace("\n", "\n" + indent)
|
|
61
|
-
record.msg = modified_message
|
|
65
|
+
# Continuation lines line up under the message, past the timestamp column.
|
|
66
|
+
indented = _with_indented_message(record, " " * len(elapsed_time + " "))
|
|
67
|
+
indented.elapsed_time = elapsed_time
|
|
62
68
|
|
|
63
|
-
return super().format(
|
|
69
|
+
return super().format(indented)
|
|
64
70
|
|
|
65
71
|
|
|
66
72
|
class LoggingHandler(logging.Handler):
|
|
@@ -132,3 +138,19 @@ def dump_crash_logs(args, run_state: RunState, formatter=None):
|
|
|
132
138
|
|
|
133
139
|
if crash_handler and args.filename:
|
|
134
140
|
crash_handler.dump_to_file(args.log_file_name, formatter)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
NARRATION_HANDLER_NAME = "headless-narration"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def stop_narrating() -> None:
|
|
147
|
+
"""Detaches the headless stdout sink once the render is over.
|
|
148
|
+
|
|
149
|
+
The summary prints with Rich and bypasses logging, so a still-attached sink reports
|
|
150
|
+
every failure twice.
|
|
151
|
+
"""
|
|
152
|
+
root_logger = logging.getLogger(LOGGER_NAME)
|
|
153
|
+
for handler in list(root_logger.handlers):
|
|
154
|
+
if handler.get_name() == NARRATION_HANDLER_NAME:
|
|
155
|
+
root_logger.removeHandler(handler)
|
|
156
|
+
handler.close()
|
plain_file.py
CHANGED
|
@@ -455,6 +455,19 @@ def process_imports(
|
|
|
455
455
|
return required_concepts
|
|
456
456
|
|
|
457
457
|
|
|
458
|
+
def normalize_line_endings(plain_source_text: str) -> str:
|
|
459
|
+
return plain_source_text.replace("\r\n", "\n").replace("\r", "\n")
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def restore_stripped_lines(plain_source_text: str, content: str) -> str:
|
|
463
|
+
stripped_source = plain_source_text.rstrip()
|
|
464
|
+
if not content or not stripped_source.endswith(content):
|
|
465
|
+
return content
|
|
466
|
+
|
|
467
|
+
stripped_line_count = stripped_source[: len(stripped_source) - len(content)].count("\n")
|
|
468
|
+
return "\n" * stripped_line_count + content
|
|
469
|
+
|
|
470
|
+
|
|
458
471
|
def read_plain_source_metadata(plain_source_text):
|
|
459
472
|
try:
|
|
460
473
|
plain_source_obj = frontmatter.loads(plain_source_text)
|
|
@@ -490,6 +503,8 @@ def parse_plain_source( # noqa: C901
|
|
|
490
503
|
imported_modules: list[str],
|
|
491
504
|
modules_trace: list[str],
|
|
492
505
|
) -> PlainFileParseResult:
|
|
506
|
+
plain_source_text = normalize_line_endings(plain_source_text)
|
|
507
|
+
|
|
493
508
|
plain_source_obj = read_plain_source_metadata(plain_source_text)
|
|
494
509
|
|
|
495
510
|
plain_source = PLAIN_SOURCE_TEMPLATE.copy()
|
|
@@ -508,7 +523,9 @@ def parse_plain_source( # noqa: C901
|
|
|
508
523
|
|
|
509
524
|
[_, loaded_templates] = file_utils.get_loaded_templates(template_dirs, plain_source_text)
|
|
510
525
|
|
|
511
|
-
|
|
526
|
+
plain_source_content = restore_stripped_lines(plain_source_text, plain_source_obj.content)
|
|
527
|
+
|
|
528
|
+
plain_source_full_text = render_plain_source(plain_source_content, loaded_templates, code_variables)
|
|
512
529
|
|
|
513
530
|
plain_file = mistletoe.Document(io.StringIO(plain_source_full_text))
|
|
514
531
|
|