plain-forge 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/bin/cli.mjs +143 -0
- package/forge/docs/.gitkeep +0 -0
- package/forge/rules/definitions.md +57 -0
- package/forge/rules/exported-concepts.md +39 -0
- package/forge/rules/func-specs.md +72 -0
- package/forge/rules/impl-reqs.md +50 -0
- package/forge/rules/import-modules.md +51 -0
- package/forge/rules/required-concepts.md +45 -0
- package/forge/rules/requires-modules.md +59 -0
- package/forge/rules/test-reqs.md +47 -0
- package/forge/skills/add-acceptance-test/SKILL.md +98 -0
- package/forge/skills/add-concept/SKILL.md +67 -0
- package/forge/skills/add-feature/SKILL.md +136 -0
- package/forge/skills/add-functional-spec/SKILL.md +81 -0
- package/forge/skills/add-functional-specs/SKILL.md +115 -0
- package/forge/skills/add-implementation-requirement/SKILL.md +73 -0
- package/forge/skills/add-resource/SKILL.md +108 -0
- package/forge/skills/add-template/SKILL.md +65 -0
- package/forge/skills/add-test-requirement/SKILL.md +68 -0
- package/forge/skills/analyze-2-func-specs/SKILL.md +102 -0
- package/forge/skills/analyze-func-specs/SKILL.md +124 -0
- package/forge/skills/analyze-if-func-spec-too-complex/SKILL.md +152 -0
- package/forge/skills/break-down-func-spec/SKILL.md +156 -0
- package/forge/skills/check-plain-env/SKILL.md +288 -0
- package/forge/skills/consolidate-concepts/SKILL.md +193 -0
- package/forge/skills/create-import-module/SKILL.md +98 -0
- package/forge/skills/create-requires-module/SKILL.md +104 -0
- package/forge/skills/debug-specs/SKILL.md +189 -0
- package/forge/skills/forge-integration/SKILL.md +443 -0
- package/forge/skills/forge-plain/SKILL.md +333 -0
- package/forge/skills/implement-conformance-testing-script/SKILL.md +247 -0
- package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +324 -0
- package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +100 -0
- package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +102 -0
- package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +92 -0
- package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +100 -0
- package/forge/skills/implement-prepare-environment-script/SKILL.md +242 -0
- package/forge/skills/implement-prepare-environment-script/assets/prepare_environment_java.sh +42 -0
- package/forge/skills/implement-prepare-environment-script/assets/prepare_environment_python.sh +81 -0
- package/forge/skills/implement-unit-testing-script/SKILL.md +133 -0
- package/forge/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +82 -0
- package/forge/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +68 -0
- package/forge/skills/implement-unit-testing-script/assets/run_unittests_java.sh +45 -0
- package/forge/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +76 -0
- package/forge/skills/implement-unit-testing-script/assets/run_unittests_python.sh +90 -0
- package/forge/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +83 -0
- package/forge/skills/init-config-file/SKILL.md +261 -0
- package/forge/skills/init-plain-project/SKILL.md +124 -0
- package/forge/skills/load-plain-reference/SKILL.md +646 -0
- package/forge/skills/plain-healthcheck/SKILL.md +132 -0
- package/forge/skills/refactor-module/SKILL.md +197 -0
- package/forge/skills/resolve-spec-conflict/SKILL.md +88 -0
- package/forge/skills/run-codeplain/SKILL.md +540 -0
- package/package.json +42 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: load-plain-reference
|
|
3
|
+
description: >-
|
|
4
|
+
Loads the full ***plain language reference into context: syntax, section types
|
|
5
|
+
(definitions, implementation reqs, test reqs, functional specs, acceptance tests),
|
|
6
|
+
concept notation, frontmatter (import/requires/required_concepts/exported_concepts),
|
|
7
|
+
templates, linked resources, module model, and authoring best practices. Use whenever
|
|
8
|
+
authoring, editing, reviewing, or debugging .plain files, or before invoking any other
|
|
9
|
+
skill that reads or writes .plain content.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# PLAIN_REFERENCE.md
|
|
13
|
+
|
|
14
|
+
## Project Overview
|
|
15
|
+
|
|
16
|
+
\*\*\*plain is a specification-driven language powered by AI that generates production-ready code from `.plain` spec files.
|
|
17
|
+
|
|
18
|
+
The `.plain` files are the source of truth. They describe what the software should do, how it should be built, and how it should be tested. The generated code is a read-only artifact produced by the renderer.
|
|
19
|
+
|
|
20
|
+
## ***plain Language Reference
|
|
21
|
+
|
|
22
|
+
***plain is a specification language designed for writing software requirements in a clear, structured format. It generates production-ready code from `.plain` spec files using AI. Full documentation: https://plainlang.org/docs/language-guide/
|
|
23
|
+
|
|
24
|
+
### .plain File Structure
|
|
25
|
+
|
|
26
|
+
A `.plain` file has a YAML frontmatter section followed by standardized sections marked with `***section name***` headers. There are four types of specification sections:
|
|
27
|
+
|
|
28
|
+
- `***definitions***` — declares concepts used throughout the specification
|
|
29
|
+
- `***implementation reqs***` — non-functional requirements about how the software should be built
|
|
30
|
+
- `***test reqs***` — requirements for conformance testing
|
|
31
|
+
- `***functional specs***` — describes what the software should do
|
|
32
|
+
|
|
33
|
+
Every plain source file requires at least one functional spec and an associated implementation req.
|
|
34
|
+
|
|
35
|
+
### Concept Notation
|
|
36
|
+
|
|
37
|
+
Concepts are the building blocks of ***plain specifications. They are written between colons: `:ConceptName:`. Valid characters include letters, digits, plus, minus, dot, and underscore.
|
|
38
|
+
|
|
39
|
+
Concepts must be defined in `***definitions***` before being referenced in other sections. Concept names must be globally unique across the specification and its imports. Concept references must not form cycles — if concept A references concept B, then concept B must not reference concept A.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
|
|
43
|
+
```plain
|
|
44
|
+
***definitions***
|
|
45
|
+
|
|
46
|
+
- :User: is the user of :App:
|
|
47
|
+
|
|
48
|
+
- :Task: describes an activity that needs to be done by :User:. :Task: has:
|
|
49
|
+
- Name - a short description (required)
|
|
50
|
+
- Notes - additional details (optional)
|
|
51
|
+
- Due Date - completion deadline (optional)
|
|
52
|
+
|
|
53
|
+
- :TaskList: is a list of :Task: items.
|
|
54
|
+
- Initially :TaskList: should be empty.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Predefined Concepts
|
|
58
|
+
|
|
59
|
+
***plain provides predefined concepts available in all specifications without needing to be defined:
|
|
60
|
+
|
|
61
|
+
| Concept | Meaning |
|
|
62
|
+
|---------|---------|
|
|
63
|
+
| `:plainDefinitions:` | Content of the `***definitions***` section |
|
|
64
|
+
| `:plainImplementationReqs:` | Content of the `***implementation reqs***` section |
|
|
65
|
+
| `:plainFunctionality:` | Content of the `***functional specs***` section |
|
|
66
|
+
| `:plainTestReqs:` | Content of the `***test reqs***` section |
|
|
67
|
+
| `:Implementation:` | The system implementing `:plainFunctionality:` |
|
|
68
|
+
| `:plainImplementationCode:` | The generated implementation code |
|
|
69
|
+
| `:UnitTests:` | Auto-generated unit tests for individual functionalities - their usage goes in in the ***implementation reqs*** section |
|
|
70
|
+
| `:ConformanceTests:` | Auto-generated tests that verify implementation conforms to specs |
|
|
71
|
+
| `:AcceptanceTest:` / `:AcceptanceTests:` | Tests that validate specific aspects of the implementation |
|
|
72
|
+
|
|
73
|
+
### Definitions Section
|
|
74
|
+
|
|
75
|
+
Declares concepts used throughout the specification. A concept must be defined before it can be referenced in any section. The definition can come from the module's own `***definitions***` section, from an `import`ed module's definitions, or from a `require`d module's `exported_concepts` (but not transitively). Attributes, constraints and clarifications can be nested as sub-bullets.
|
|
76
|
+
|
|
77
|
+
```plain
|
|
78
|
+
***definitions***
|
|
79
|
+
|
|
80
|
+
- :ConceptName: is a description of the concept.
|
|
81
|
+
- Additional details or attributes can be nested
|
|
82
|
+
- Multiple attributes can be listed
|
|
83
|
+
- Concept clarification also goes here
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Implementation Reqs Section
|
|
87
|
+
|
|
88
|
+
A free-form section for any instructions that steer code generation. Common uses include technology choices, architectural constraints, coding standards, and naming conventions, but it can also contain detailed implementation guidance — data formats, error handling strategies, algorithm descriptions, or any other context the renderer needs to produce correct code. These describe HOW to build the software, not WHAT it should do. Specs about unit testing also go here - it is a common mistake to include them in the `***test reqs***` section.
|
|
89
|
+
|
|
90
|
+
```plain
|
|
91
|
+
***implementation reqs***
|
|
92
|
+
|
|
93
|
+
- :Implementation: should be in Python.
|
|
94
|
+
|
|
95
|
+
- :MainExecutableFile: of :App: should be called "hello_world.py".
|
|
96
|
+
|
|
97
|
+
- :Implementation: should include :Unittests: using Unittest framework!
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Test Reqs Section
|
|
101
|
+
|
|
102
|
+
Specifies requirements for conformance testing — test frameworks, execution methods, and testing constraints. Only used when writing and fixing conformance tests (not unit tests). Unit tests should be specified in the `***implementation reqs***` section and NOT HERE.
|
|
103
|
+
|
|
104
|
+
```plain
|
|
105
|
+
***test reqs***
|
|
106
|
+
|
|
107
|
+
- :ConformanceTests: of :App: should be implemented in Python using Unittest framework.
|
|
108
|
+
|
|
109
|
+
- :ConformanceTests: will be run using "python -m unittest discover" command.
|
|
110
|
+
|
|
111
|
+
- :ConformanceTests: must be implemented and executed - do not use unittest.skip().
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Functional Specs Section
|
|
115
|
+
|
|
116
|
+
Describes what the software should do. Each bullet point is a single piece of functionality that will be implemented. Functional specs are rendered incrementally one by one — earlier specs cannot reference later specs.
|
|
117
|
+
|
|
118
|
+
Each functional spec must be limited in complexity. If a spec is too complex, the renderer responds with "Functional spec too complex!" and it must be broken down into smaller specs. Complexity is measured in lines of code - each spec should imply more than 200 lines of code.
|
|
119
|
+
|
|
120
|
+
Functional specs are in **chronological order** — earlier specs are rendered before later ones. Functional specs defined in `requires` modules are considered **previous functional specs** relative to the current module's specs. This ordering matters for incremental rendering and for detecting conflicts between specs.
|
|
121
|
+
|
|
122
|
+
The renderer has **no knowledge of future functional specs**. When a functional spec is being implemented, only the previous functional specs (those already rendered) are in the renderer's context. Specs that come later in the list are invisible to the renderer at that point. This means each spec is implemented without any awareness of what will come next.
|
|
123
|
+
|
|
124
|
+
```plain
|
|
125
|
+
***functional specs***
|
|
126
|
+
|
|
127
|
+
- Implement the entry point for :App:.
|
|
128
|
+
|
|
129
|
+
- Show :TaskList:.
|
|
130
|
+
|
|
131
|
+
- :User: should be able to add :Task:. Only valid :Task: items can be added.
|
|
132
|
+
|
|
133
|
+
- :User: should be able to delete :Task:.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Each functional spec must be unambiguous. If a single line is not enough to fully disambiguate the behavior, use nested sub-bullets to add detail. Nested lines clarify the parent spec — they do not introduce separate functionality. Even with nested detail, the spec must still respect the complexity limit.
|
|
138
|
+
|
|
139
|
+
```plain
|
|
140
|
+
***functional specs***
|
|
141
|
+
|
|
142
|
+
- :User: should be able to send a :Message: to a :Conversation:.
|
|
143
|
+
- A :Message: must have non-empty content.
|
|
144
|
+
- The :Message: is appended to the end of the :Conversation:.
|
|
145
|
+
- All :Participant: members of the :Conversation: can see the new :Message:.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Acceptance Tests
|
|
149
|
+
|
|
150
|
+
Nested under individual functional specs to specify how to verify correct implementation. They extend conformance tests and are implemented according to the `***test reqs***` specification. Acceptance tests are only run if conformance tests are enabled.
|
|
151
|
+
|
|
152
|
+
```plain
|
|
153
|
+
***functional specs***
|
|
154
|
+
|
|
155
|
+
- Display "hello, world"
|
|
156
|
+
|
|
157
|
+
***acceptance tests***
|
|
158
|
+
- :App: should exit with status code 0 indicating successful execution.
|
|
159
|
+
- :App: should complete execution in under 1 second.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### YAML Frontmatter
|
|
163
|
+
|
|
164
|
+
The frontmatter is enclosed between `---` markers and supports:
|
|
165
|
+
|
|
166
|
+
- **`import`** — includes definitions, implementation reqs, and test reqs from templates. Imported modules must not contain functional specs. The default import directory is `template/` — the `template/` prefix is not needed (e.g., `airplain` resolves to `template/airplain.plain`).
|
|
167
|
+
- **`requires`** — specifies dependencies on other root-level modules that must be built first. Unlike `import`, required modules can contain functional specs and represent complete software modules. Requires paths point to root-level modules (e.g., `auth`, `messaging`).
|
|
168
|
+
- **`description`** — optional description of the specification.
|
|
169
|
+
- **`required_concepts`** — concepts that must be defined by any module that imports this spec.
|
|
170
|
+
- **`exported_concepts`** — concepts made available to modules that `require` this one. **Exports are not transitive.** A concept exported from module `A` is visible only to the modules that `requires: A` directly. If module `B` `requires: A` and module `C` `requires: B`, the concepts `A` exports are **not** automatically visible to `C` — only the concepts `B` itself re-exports are. To pass a concept further down the chain, the intermediate module must re-declare it in its own `exported_concepts` list (and define / forward it in its own `***definitions***` as needed). This applies at every hop: each module is responsible for explicitly exporting whatever it wants its own `requires`-ers to see.
|
|
171
|
+
|
|
172
|
+
**When a concept needs to live in more than just the immediately next module, don't propagate it by chained re-exports** — that turns every intermediate module into bookkeeping for a concept it doesn't itself use, and any missing hop silently drops the concept from downstream modules. Use a **shared import module** instead:
|
|
173
|
+
|
|
174
|
+
1. Create an import module under `template/` (e.g. `template/shared_domain.plain`) and put the concept's `***definitions***` entry there. Import modules carry definitions, implementation reqs, and test reqs only — never functional specs.
|
|
175
|
+
2. In every module that needs the concept (no matter how deep in the `requires` chain), add the import module to its frontmatter `import:` list. The concept is then visible in that module directly, without any `exported_concepts` plumbing.
|
|
176
|
+
3. None of the `requires`-chained modules need to re-export the concept anymore — each one imports what it actually uses.
|
|
177
|
+
|
|
178
|
+
Use the `create-import-module` skill to scaffold this, and `consolidate-concepts` when you discover the same concept has been scattered across several modules and needs to be pulled back into a single shared import.
|
|
179
|
+
|
|
180
|
+
Rule of thumb: if a concept crosses **one** hop, `exported_concepts` is fine. If it crosses **two or more** hops, or is needed by sibling modules at the same depth, lift it into an import module.
|
|
181
|
+
|
|
182
|
+
### Linked Resources
|
|
183
|
+
|
|
184
|
+
Specifications can reference external files using markdown link syntax. The linked resource is passed along with the spec to the renderer. File paths are resolved relative to the `.plain` file location. Only files in the same folder (and subfolders) are supported.
|
|
185
|
+
|
|
186
|
+
```plain
|
|
187
|
+
- :User: should be able to add :Task:. The details of the user interface
|
|
188
|
+
are provided in the file [task_modal_specification.yaml](task_modal_specification.yaml).
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### Hard constraint: a linked resource is always a single, text-based file on disk
|
|
192
|
+
|
|
193
|
+
The renderer reads the linked file's bytes verbatim and feeds them into the model alongside the spec. That mechanism only works for a specific shape of target, and violating any of the three rules below is one of the most common and disruptive mistakes in `.plain` authoring — the spec **looks** valid, but the renderer either silently ignores the link, fails to read it, or wastes the model's context window on bytes it cannot interpret.
|
|
194
|
+
|
|
195
|
+
A linked resource **must not** be any of the following:
|
|
196
|
+
|
|
197
|
+
1. **A folder / directory.** `[integrations](src/integrations/)`, `[schemas](resources/schemas/)`, `[host project](../host_project/)` are all invalid — the renderer cannot ingest a directory. If a whole directory's worth of content is relevant, pick the single most representative **file** inside it (a `README.md`, an exemplar source file, a manifest at the directory root) and link **that**.
|
|
198
|
+
2. **A URL / external location.** `[Stripe docs](https://stripe.com/docs/api)`, any `http://` / `https://` / `ftp://` / `git://` / `s3://` / `gs://` target. Linked resources are local-file only. If a URL's content is essential to the spec, fetch it once, save the response to a text file under `resources/` (e.g. `resources/stripe-docs-snapshot.md`, `resources/example-openapi.yaml`), and link **that file**.
|
|
199
|
+
3. **A binary file.** PNG, JPG, JPEG, GIF, BMP, TIFF, WebP, ICO, PDF, DOCX, XLSX, PPTX, ZIP, TAR, GZ, MP3, MP4, WAV, compiled binaries (`.exe`, `.so`, `.dylib`, `.class`, `.wasm`), and anything else that isn't human-readable text in its raw form. Binary content cannot be meaningfully consumed by the renderer; linking a screenshot, a PDF spec, or a packaged artifact accomplishes nothing except bloating the context. If the information in a binary asset is essential, transcribe it into a text-based form first — a UI screenshot becomes a Markdown description or a structured YAML wireframe under `resources/`; a PDF spec becomes a Markdown extract or the underlying JSON Schema / OpenAPI; an architecture diagram becomes a Mermaid block inside a Markdown file.
|
|
200
|
+
|
|
201
|
+
If the markdown-link target ends with `/`, contains `://`, points at a path that resolves to a directory, or points at a file with one of the binary extensions above, **stop** — it cannot be a linked resource. Convert it to a text file under `resources/` first, then link the converted file.
|
|
202
|
+
|
|
203
|
+
#### URLs and folder paths must not appear *anywhere* in `.plain` content
|
|
204
|
+
|
|
205
|
+
The constraint above is **not** just about markdown link syntax. URLs (any `http://`, `https://`, `ftp://`, `git://`, `s3://`, … string) and folder paths (`src/integrations/`, `../host_project/`, anything ending with `/`, anything that resolves to a directory) **must not appear anywhere in `.plain` content** — not as link targets, not in concept body prose, not in functional-spec text, not in implementation reqs, not in test reqs. Mentioning a URL or a folder in prose is a critical and common mistake because:
|
|
206
|
+
|
|
207
|
+
- **The renderer cannot follow URLs or open folders.** A URL or folder reference in prose is a *ghost* dependency: it looks meaningful to a human reader, but it contributes nothing to code generation. Worse, downstream readers (and future you) assume the renderer used the referenced content, so the spec silently drifts from reality.
|
|
208
|
+
- **The fix is always the same**: if external content matters, fetch it (or pick one canonical file out of the directory), save it as a text file under `resources/`, and refer to it through a normal linked resource. The concept or spec then names the content through the linked file, not through a URL or folder path string.
|
|
209
|
+
|
|
210
|
+
The **only** exception is for URLs and paths that are *values the produced software itself uses at runtime* — the base URL the integration calls, a database connection path, a CLI argument default. Those are configuration values, not external references, and they belong in the spec because the generated code needs them. A useful litmus test: "Would the renderer benefit from reading the bytes at this URL / folder?" If yes, save it to a text file and link the file. If no (it's a runtime value the generated code carries forward), it can stay as plain text in the spec.
|
|
211
|
+
|
|
212
|
+
**Structured protocol artifacts must be linked resources, never transcribed into prose.** Anything that has a formal machine-readable shape which includes but is not limited to — JSON Schema, OpenAPI / Swagger specs, GraphQL SDL, Protobuf / gRPC `.proto` files, Avro / Thrift schemas, XML XSDs, AsyncAPI specs, JSON-RPC method definitions, wire-protocol descriptions, payload examples, etc. — belongs in a file under `resources/` (or a subfolder of the `.plain` file's directory), and the spec refers to it via a markdown link. Do **not** restate the schema's fields, types, or constraints inline in functional specs, implementation reqs, or definitions. Reasons:
|
|
213
|
+
|
|
214
|
+
- **One source of truth.** A re-typed copy of a schema in prose drifts as soon as the real schema evolves. Both the renderer *and* downstream tooling (codegen, validators, API clients, IDE plugins) need the same canonical file.
|
|
215
|
+
- **Machine-readable.** The renderer and the generated code can both consume the file directly — a JSON Schema linked from a spec can drive request/response validation in the implementation *and* assertions in conformance tests, with no prose-to-code translation step in between.
|
|
216
|
+
- **Reviewable as a diff.** Schema changes show up cleanly in PRs as edits to a single file, instead of as a scatter of edits across multiple `.plain` sections.
|
|
217
|
+
|
|
218
|
+
The accompanying spec line should describe the *role* of the artifact ("the request body conforms to ...", "the public API surface is defined in ...") rather than its contents. If the artifact is referenced from more than one place, follow the [single-reference + concept](#linked-resources) rule below.
|
|
219
|
+
|
|
220
|
+
```plain
|
|
221
|
+
***definitions***
|
|
222
|
+
|
|
223
|
+
- :TaskCreateRequest: is the JSON payload for creating a task, defined by
|
|
224
|
+
[resources/task_create_request.schema.json](resources/task_create_request.schema.json).
|
|
225
|
+
- :TasksAPI: is the public HTTP surface for tasks, defined by
|
|
226
|
+
[resources/tasks_openapi.yaml](resources/tasks_openapi.yaml).
|
|
227
|
+
|
|
228
|
+
***functional specs***
|
|
229
|
+
|
|
230
|
+
- :User: should be able to add :Task: by POSTing :TaskCreateRequest: to the
|
|
231
|
+
`POST /tasks` endpoint of :TasksAPI:. The endpoint responds per :TasksAPI:.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Each linked resource must be referenced from exactly one place** in the project — a single functional spec, implementation requirement, or `***definitions***` entry. Linking the same file from two functional specs (or from a functional spec *and* a requirement, etc.) creates two independent sources of truth: any later edit to one site silently diverges from the other, and the renderer has no way to know which one is canonical.
|
|
235
|
+
|
|
236
|
+
If a resource needs to inform multiple parts of the project, **don't repeat the link** — instead, attach the resource to a **concept** and let the rest of the project refer to that concept:
|
|
237
|
+
|
|
238
|
+
1. Define a concept under `***definitions***` whose explanation links the resource exactly once.
|
|
239
|
+
2. Use the concept token (`:ConceptName:`) wherever the resource was previously inlined.
|
|
240
|
+
|
|
241
|
+
For example, instead of linking `task_modal_specification.yaml` from two different functional specs:
|
|
242
|
+
|
|
243
|
+
```plain
|
|
244
|
+
***definitions***
|
|
245
|
+
|
|
246
|
+
- :TaskModalSpec: is the user-interface contract for the task modal,
|
|
247
|
+
fully described in [task_modal_specification.yaml](task_modal_specification.yaml).
|
|
248
|
+
|
|
249
|
+
***functional specs***
|
|
250
|
+
|
|
251
|
+
- :User: should be able to add :Task: using :TaskModalSpec:.
|
|
252
|
+
|
|
253
|
+
- :User: should be able to edit :Task: using :TaskModalSpec:.
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
This keeps the resource link in one place, makes the dependency explicit through the concept token, and means a change to the file only ever needs to be reconciled against one spec site. If you find yourself about to paste the same `[name](path)` link a second time, **stop** — create the concept first.
|
|
257
|
+
|
|
258
|
+
### Template System
|
|
259
|
+
|
|
260
|
+
***plain supports template inclusion using `{% include %}` syntax:
|
|
261
|
+
|
|
262
|
+
```plain
|
|
263
|
+
{% include "python-console-app-template.plain", main_executable_file_name: "my_app.py" %}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Parameters are passed as key-value pairs. Inside the template, they are accessed using variable syntax (`{{ variable_name }}`). Only variables are supported — conditionals, loops, and other Liquid features are not available.
|
|
267
|
+
|
|
268
|
+
### Comments
|
|
269
|
+
|
|
270
|
+
Lines starting with `>` are ignored when rendering:
|
|
271
|
+
|
|
272
|
+
```plain
|
|
273
|
+
> This is a comment in ***plain
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Best Practices
|
|
277
|
+
|
|
278
|
+
1. **Reference concepts consistently** — use `:ConceptName:` notation to disambiguate key concepts
|
|
279
|
+
2. **Keep it simple** — specs should be readable by both humans and AI
|
|
280
|
+
3. **Leverage templates** — use the standard template library for common patterns
|
|
281
|
+
4. **Use acceptance tests** — add them for requirements that need verification (under the condition that conformance tests are enabled)
|
|
282
|
+
5. **Be specific** — write clear, testable requirements in functional specs
|
|
283
|
+
6. **Define before use** — always define concepts in `***definitions***` before referencing them
|
|
284
|
+
7. **Start with imports** — import relevant templates before defining your own concepts
|
|
285
|
+
|
|
286
|
+
## Repository Structure
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
*.plain # Specification files (the source of truth)
|
|
290
|
+
template/*.plain # Reusable template specs imported by module specs
|
|
291
|
+
plain_modules/ # Generated code output (one folder per .plain spec)
|
|
292
|
+
resources/ # Schemas, API specs, transforms, test fixtures
|
|
293
|
+
conformance_tests/ # Generated conformance tests (one folder per module)
|
|
294
|
+
test_scripts/ # Scripts for running unit and conformance tests
|
|
295
|
+
config.yaml # codeplain CLI configuration
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Generated artifacts** (gitignored):
|
|
299
|
+
- `plain_modules/<module_name>/` — generated project for each `.plain` spec (implementation + unit tests)
|
|
300
|
+
- `conformance_tests/<module_name>/` — generated conformance tests for each module
|
|
301
|
+
|
|
302
|
+
## How Modules Work
|
|
303
|
+
|
|
304
|
+
There are two types of modules:
|
|
305
|
+
|
|
306
|
+
### Import Modules
|
|
307
|
+
|
|
308
|
+
An import module may live in the **`template/`** directory (other directories are also supported) and contains **only** `***definitions***`, `***implementation reqs***`, and/or optionally `***test reqs***`. It must **not** contain `***functional specs***` and must **not** use `requires`. It may optionally `import` other templates for layered reuse.
|
|
309
|
+
|
|
310
|
+
When a module **`import`s** another, it gains access to the imported module's definitions, implementation reqs, and test reqs — but not its functional specs. The default import directory needs to be specified in `config.yaml` - in such a case, the directory prefix is not needed (e.g., `airplain`).
|
|
311
|
+
|
|
312
|
+
### Requires Modules
|
|
313
|
+
|
|
314
|
+
`requires` establishes a build ordering between modules. The required module is built **before** the current one. This does not necessarily mean the current module extends or depends on the required module's code — it may be completely independent. The `requires` relationship ensures the build order is correct.
|
|
315
|
+
|
|
316
|
+
When a module **`requires`** another:
|
|
317
|
+
- The required module's generated code (`plain_modules/<required_module>`) is copied as the starting point.
|
|
318
|
+
- The required module's `***functional specs***` become visible as **previous functional specs** - this property IS transitive.
|
|
319
|
+
- Only `exported_concepts` from the required module are available (not its full definitions) - this property IS NOT transitive.
|
|
320
|
+
|
|
321
|
+
A module can use both `requires` and `import` together.
|
|
322
|
+
|
|
323
|
+
**`requires` modules must share the same tech stack.** Because the required module's generated code is copied as the starting point and the renderer continues building on top of it with one language/framework toolchain, two modules can only be linked with `requires` when they target the same language, framework, and runtime. A runtime/network dependency between systems is **not** a reason to use `requires`. For example, a React frontend that talks to a Python/FastAPI backend over HTTP must **not** `requires: [backend]` — the stacks differ. Model that pair as two independent root modules (each with its own `config.yaml` and test scripts), and express the contract between them through a shared API schema in `resources/` or shared concepts in an `import`ed template, NOT through `requires`.
|
|
324
|
+
|
|
325
|
+
### Contracts Between Modules
|
|
326
|
+
|
|
327
|
+
Modules can use `required_concepts` and `exported_concepts` to enforce contracts between them. An import module declaring `required_concepts` means any module that imports it must define those concepts. A module declaring `exported_concepts` controls which concepts are visible to modules that `require` it - not transitive.
|
|
328
|
+
|
|
329
|
+
**Exported concepts are not transitive.** If module A exports a concept and module B `requires` A, module B can use that concept — but if module C `requires` B, it does **not** automatically gain access to A's exported concepts. If a concept needs to be shared across multiple `requires` modules, define it in a common import module and have each module `import` that shared template.
|
|
330
|
+
|
|
331
|
+
## Conformance Test Workflow
|
|
332
|
+
|
|
333
|
+
Each functional spec in a module has its own set of conformance tests, generated per functional spec per module. After a new functional spec is rendered (i.e., its implementation code is generated), conformance tests for that spec are also rendered. Before proceeding, **all previous conformance tests** (from earlier functional specs in the same module) are run. Ideally, all conformance tests of all previous functional specs pass without any changes. If any previously passing conformance test now fails, the failure must be resolved before moving on. Resolution means one of three things: fixing the conformance test, fixing the implementation code (by adjusting the spec), or identifying conflicting specs.
|
|
334
|
+
|
|
335
|
+
If conformance tests of a previous functional spec need to be changed in order to pass, this is a strong indicator that the functional specs themselves may need to be amended. Needing to modify earlier conformance tests suggests the new functional spec has introduced behavior that is inconsistent with what was previously specified — the specs should be reviewed and clarified to eliminate the ambiguity or conflict.
|
|
336
|
+
|
|
337
|
+
## Running Tests
|
|
338
|
+
|
|
339
|
+
Test scripts live in `test_scripts/` and are run from the repo root:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
# Run all unit tests for a module
|
|
343
|
+
./test_scripts/run_unittests.sh plain_modules/<module_name>
|
|
344
|
+
|
|
345
|
+
# Prepare environment for conformance tests
|
|
346
|
+
./test_scripts/prepare_environment.sh plain_modules/<module_name>
|
|
347
|
+
|
|
348
|
+
# Run conformance tests for a specific functionality in a module
|
|
349
|
+
./test_scripts/run_conformance_tests.sh plain_modules/<module_name> conformance_tests/<module_name>/<functionality>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Testing Scripts
|
|
353
|
+
|
|
354
|
+
Every ***plain project ships shell scripts under `test_scripts/` that the user (and the renderer) call into to verify the generated code. There are three kinds, each authored by a dedicated skill — use the corresponding skill as the source of truth whenever you create, regenerate, or adapt a script.
|
|
355
|
+
|
|
356
|
+
### Why these scripts exist (and why they're shaped the way they are)
|
|
357
|
+
|
|
358
|
+
The **primary** purpose of these scripts is **automated execution by the renderer** to validate the generated code and validate that all the previous functionalities work as expected, not manual invocation by a developer. The user *can* run them by hand (see [Running Tests](#running-tests)), but the renderer runs them many times more often — once for every functional spec it processes — as part of its incremental rendering loop. The contract between the scripts and the renderer is shaped by that execution model:
|
|
359
|
+
|
|
360
|
+
- **Conformance tests are per-functional-spec.** Each functional spec in a module has its own folder under `conformance_tests/<module>/<functionality>/`. After the renderer finishes generating code for a new functional spec and the unit tests and refactoring passes, it runs the conformance tests of **all previous functional spec** to detect regressions — see [Conformance Test Workflow](#conformance-test-workflow). For a single module (with 0 `requires` modules) with N functional specs, the conformance script gets invoked **on the order of N times per render**, each invocation pointing at a different spec's test folder.
|
|
361
|
+
- **Each per-spec invocation is independent.** The conformance script does not know that it's the second invocation in a long sequence; from its point of view, each invocation is a cold start against a single spec's tests.
|
|
362
|
+
- **Per-spec independence is also what makes dependency installation expensive.** A naive conformance runner would re-install all of the project's runtime dependencies (Python venv + `pip install`, Maven dependency tree, `npm ci`, `cargo build`, ...) on every one of those N invocations. That's `N × install-cost` of wasted work for every render.
|
|
363
|
+
- **That is exactly why `prepare_environment_<lang>` exists.** Its **only** job is to amortize the install cost: install once at the start of a render, populate `.tmp/<lang>_<arg>/` with the warmed dependency cache and build artifacts, then let the conformance runner **attach** to that working folder on each of the N per-spec invocations instead of re-installing. The conformance runner's [activate-only variant](../implement-conformance-testing-script/SKILL.md#variant-decision-install-inline-vs-activate-only) does precisely that. When no prepare script exists, the conformance runner falls back to the install-inline variant and pays the install cost N times — acceptable for tiny projects, costly for anything realistic.
|
|
364
|
+
- **The unit-test runner has a different execution model, because unit tests live in a different place.** Unit tests are part of the generated codebase itself — they sit directly inside `plain_modules/<module>/` next to the implementation they exercise — whereas conformance tests live *outside* the codebase, in their own per-spec folders under `conformance_tests/<module>/<spec>/`. As a result, the unit-test runner doesn't have a per-spec axis to iterate over: it just runs against the whole `plain_modules/<module>/` build in one go, gets invoked far fewer times per render, and has no amortization gain to chase. That's why the unit-test runner is always self-contained and there is no `prepare_environment`-equivalent for it.
|
|
365
|
+
|
|
366
|
+
Keep this framing in mind when you author or adapt any of these scripts. The decisions about working-folder paths, isolation locations, exit codes, and the activate-only-vs-install-inline split are not arbitrary house style — they are what makes the renderer's per-spec loop tractable.
|
|
367
|
+
|
|
368
|
+
### The three scripts
|
|
369
|
+
|
|
370
|
+
- **`run_unittests_<lang>.sh` / `.ps1`** — runs the auto-generated unit tests in `plain_modules/<module>/`. Authored by the [`implement-unit-testing-script`](../implement-unit-testing-script/SKILL.md) skill. Receives one positional argument: the source build folder name. Invoked roughly once per render. **Fully self-contained:** it installs its own dependencies inline (via `pip install -r requirements.txt`, `npm ci`, `mvn`, `cargo fetch`, etc.) and never relies on any other script having run first.
|
|
371
|
+
- **`run_conformance_tests_<lang>.sh` / `.ps1`** — runs the conformance tests in `conformance_tests/<module>/<spec>/` against the generated implementation. Authored by the [`implement-conformance-testing-script`](../implement-conformance-testing-script/SKILL.md) skill. Receives two positional arguments: the source build folder and the conformance tests folder. **Invoked once per previous functional spec on every render** — i.e. roughly N times for a module with N functional specs.
|
|
372
|
+
- **`prepare_environment_<lang>.sh` / `.ps1`** — *optional* one-time setup that runs **before** the conformance script and **only the conformance script**. Invoked **once per render** to install the build's dependencies and pre-warm build artifacts into `.tmp/<lang>_<arg>/` so the N subsequent conformance invocations can attach to the warmed environment instead of re-installing. Authored by the [`implement-prepare-environment-script`](../implement-prepare-environment-script/SKILL.md) skill. Receives one positional argument: the source build folder name. **It does not feed the unit-test script** — see [`prepare_environment` is conformance-only](#prepare_environment-is-conformance-only-common-mistake) below.
|
|
373
|
+
|
|
374
|
+
### `prepare_environment` is conformance-only (common mistake)
|
|
375
|
+
|
|
376
|
+
It is a **common and costly mistake** to assume that `prepare_environment_<lang>` is a generic "warm up the environment for all the testing scripts" step that the unit-test runner can also lean on. It is not. The hard rule:
|
|
377
|
+
|
|
378
|
+
> `prepare_environment_<lang>` exists **solely** to set up the working folder that `run_conformance_tests_<lang>` then attaches to (the activate-only variant). The unit-test runner (`run_unittests_<lang>`) is **completely independent and complete** of it — it does not read from `prepare`'s working folder, does not require `prepare` to have run, and must install whatever dependencies it needs on its own.
|
|
379
|
+
|
|
380
|
+
Why:
|
|
381
|
+
|
|
382
|
+
- **Unit tests run against `plain_modules/<module>/`, conformance tests run against `.tmp/<lang>_<arg>/`.** The two scripts stage into different places. `prepare_environment` populates `.tmp/<lang>_<arg>/` for conformance; the unit-test script does its own staging into its **own** `.tmp/<lang>_<arg>/` working folder and installs its own dependencies there.
|
|
383
|
+
- **The unit-test runner must work in isolation.** Users and CI systems run unit tests as a quick smoke check without ever invoking conformance. If `run_unittests_<lang>` depended on `prepare_environment` having run, those one-off unit-test invocations would silently fail (or be "fixed" by a misguided edit to make it depend on `prepare`).
|
|
384
|
+
- **The skill contract enforces it.** [`implement-unit-testing-script`](../implement-unit-testing-script/SKILL.md) emits a fully self-contained script every time: toolchain check → stage → install dependencies inline → run tests. It never emits an activate-only variant. The two-variant pattern is exclusive to the conformance runner.
|
|
385
|
+
|
|
386
|
+
If you find yourself authoring (or asked to author) a `prepare_environment` script that handles unit-test dependencies too, **stop**. The unit-test script handles its own dependencies. Adding a unit-test path into `prepare_environment` couples scripts that should stay independent, and breaks the activate-only contract between `prepare` and `conformance`.
|
|
387
|
+
|
|
388
|
+
### Shared rules across all three scripts
|
|
389
|
+
|
|
390
|
+
Anything not listed here is documented in the individual skill file:
|
|
391
|
+
|
|
392
|
+
- **Input folders are read-only — hard rule.** The build folder (and, for conformance, the conformance tests folder too) is **input only**. Every install, build artifact, cache, log, JUnit XML, coverage report, compiled test class, and temp file must land inside `.tmp/<lang>_<arg>`, never inside the input folder. The build folder is shared with the renderer and with the user's version control; writing into it corrupts both. If you find yourself about to issue a command whose `cwd` is an input folder, or whose target path starts with the input folder, **stop** — the write has to go into `.tmp/<lang>_<arg>`.
|
|
393
|
+
- **Shell flavor matches the host.** `.sh` on macOS / Linux, `.ps1` on Windows. A project intended for both OSes ships both files in matching pairs (`prepare` + `conformance` for each language must agree on working-folder name and isolation paths).
|
|
394
|
+
- **Exit codes are part of the contract.** `69` for unrecoverable errors (missing toolchain, bad args, can't enter the working folder, install failed); `1` for the "no tests discovered" guard in the conformance runner (and bad usage in the unit-test runner); any other non-zero code is propagated from the underlying test command. Other skills — notably [`plain-healthcheck`](../plain-healthcheck/SKILL.md) and [`check-plain-env`](../check-plain-env/SKILL.md) — branch on these codes.
|
|
395
|
+
- **Wired in via `config.yaml`.** Each script that is actually generated must be referenced from the relevant `config.yaml` via the `unittests-script:`, `conformance-tests-script:`, and `prepare-environment-script:` keys respectively. See the [`init-config-file`](../init-config-file/SKILL.md) skill for the canonical assembly. **If `prepare-environment-script` is declared, `conformance-tests-script` must be declared too** — a prepare script only makes sense in service of conformance, and `plain-healthcheck` will hard-fail a project that violates this.
|
|
396
|
+
- **Conformance scripts come in two variants — unit-test scripts do not.** When a `prepare_environment_<lang>` script exists, the conformance script is the **activate-only** variant (it attaches to the env prepare populated in `.tmp/`). When no prepare exists, the conformance script is the **install-inline** variant (it stages and installs in one shot). The `implement-conformance-testing-script` skill picks the right variant automatically based on whether a prepare script is already on disk. **The unit-test script has no activate-only variant** — it is always self-contained, regardless of whether a `prepare_environment_<lang>` script exists.
|
|
397
|
+
- **Dependency isolation is project-local.** Each language's package cache / virtual env / build repo lives inside the working folder (`./.venv` for Python, `./node_modules` for Node, `./.m2` for Java, `./.gocache` for Go, `./.cargo` for Rust, `./.pub-cache` for Flutter, ...) — never in the user's home directory. The conformance script reads from the same project-local location prepare wrote to; the unit-test script uses its **own** working folder and its **own** copy of the isolated dependencies.
|
|
398
|
+
- **No language-package checks live in these scripts.** The scripts themselves install language packages via `pip install -r requirements.txt`, `npm ci`, `mvn -Dmaven.repo.local=...`, `go mod download`, `cargo fetch`, etc. They do **not** pre-verify individual packages; that's the package manager's job. The host-level checks for the toolchains and external dependencies belong in `check-plain-env`, not in these scripts.
|
|
399
|
+
- **Scripts are verbose**. They print out every step they take, including toolchain checks, dependency installations, and test results. This makes it easier to debug and understand what's going on.
|
|
400
|
+
|
|
401
|
+
For implementation details — the exact step sequence, toolchain checks, language-specific install / test commands, working-folder lifecycle, anti-patterns — open the corresponding `implement-*-testing-script` skill. Do not hand-author a testing script from scratch; route every creation or modification through the matching skill so the shared rules above are enforced uniformly.
|
|
402
|
+
|
|
403
|
+
## Writing Functional Specs
|
|
404
|
+
|
|
405
|
+
- Each functional spec must imply a **maximum of 200 changed lines of code**. This is a hard limit — if a spec would result in more than 200 lines of changes, it must be broken down into smaller, independent specs. This limit also helps avoid "Functional spec too complex!" errors from the renderer.
|
|
406
|
+
- **Conflicting specs must be avoided at all costs.** Functional specs should be written so that no conflicts exist between them. If two specs appear to conflict, they must be clarified by adding more detail and context to the specs until all possible conflicts are resolved. Prevention is always preferable to debugging conflicts after rendering.
|
|
407
|
+
- **Specs should be language-agnostic.** Avoid using programming language-specific terminology (e.g., generics syntax, framework annotations, language-specific collection types, decorator syntax, language-specific base classes or type keywords like "POJO" or "dataclass") in functional specs and definitions. Write specs in terms of behavior, concepts, and domain logic — not implementation constructs. General technical terms that are not language-specific are fine (e.g., null values, JSON types, HTTP status codes, REST api endpoints etc.). The `***implementation reqs***` section is the appropriate place for language-specific guidance.
|
|
408
|
+
|
|
409
|
+
Naming concrete *components* — classes, methods, functions, fields — is encouraged and not in conflict with this rule. A functional spec should freely refer to `:CsvToJsonConverter:` as a component with methods `:CsvToJson:` and `:JsonToCsv:`, describe their inputs, outputs, and error behavior, and treat those names as part of the public contract. What it must *not* do is bake in how that contract is realized in a particular language: no `@staticmethod` decorators, no `class Foo extends Bar` phrasing, no `List<T>` or `Optional<T>` syntax, no "POJO with static methods" framing.
|
|
410
|
+
|
|
411
|
+
The litmus test: if you switched the project from Python to Java (or vice versa), would the functional spec still read correctly with only `***implementation reqs***` updated? If yes, the spec is language-agnostic. If the functional spec itself would need rewording because it referenced a language-specific construct, the construct belongs in implementation reqs instead. The component name (`:CsvToJsonConverter:`) is the same across languages; the syntax used to express "static method on a class" is not.
|
|
412
|
+
- **Keep sentences short and clear — but never at the cost of ambiguity.** Spec lines should be easy to read and understand at a glance. Prefer short, direct sentences and plain words over long sentences and jargon — if a 10-cent word and a 50-cent word say the same thing, use the 10-cent one. This applies to every spec section, not only functional specs: `***definitions***`, `***implementation reqs***`, `***test reqs***`, and `***acceptance tests***` should all be as concise as they can be while staying unambiguous. The hard constraint is in the second half of that rule: **wordy-but-precise always beats terse-but-ambiguous.** If trimming a clause, a qualifier, or a sub-bullet would leave the spec open to more than one reasonable interpretation, leave it in. When a sentence starts to grow because the behavior is genuinely complex, split it into two short sentences (or into a parent line + sub-bullets) rather than dropping detail. Concision is in service of clarity, never the other way around.
|
|
413
|
+
- **Specs must be deterministic enough to both *run* and *use* the software without reading the generated code.** A developer should be able to figure out, from the specs alone, two distinct things:
|
|
414
|
+
|
|
415
|
+
1. **How to run the built software** — the entry-point command (e.g. `python -m app`, `uvicorn app.main:app`, `./my-cli`), prerequisites (required runtime versions, package managers, system binaries), required environment variables, ports the software listens on, configuration file paths and shapes, and any default arguments.
|
|
416
|
+
2. **How to use the running software** — the full interaction surface. For a REST API: every endpoint path, HTTP method, request body shape, response body shape, status codes, and authentication scheme. For a CLI tool: every command, its arguments and flags, the expected output (including exit codes), and the input it reads (stdin, files, env vars). For a library: every public function/class, its signature, the inputs it accepts, the outputs it returns, and the errors it can raise.
|
|
417
|
+
|
|
418
|
+
Concretely, a reader should never have to open `plain_modules/` to answer "how do I start this?" or "how do I call this endpoint?" — those answers must already live in the specs. **Never leave runtime or interface details up to the renderer's discretion** — if the spec doesn't pin them down, two renders can produce two different shapes, and any human or automated consumer of the software is now coupled to an undocumented choice.
|
|
419
|
+
- **Encapsulate functionality in functional specs.** `requires` modules import only functional specs. It is therefore important that the functionality is encapsulated in the functional specs and not in implementation reqs, as those will not be in the context of future functional specs when fixing previous conformance tests of previous functional specs.
|
|
420
|
+
- **Specs must define programmatic interfaces.** Any runtime or interface details must be defined in the functional specs, not in implementation reqs. This means functional specs name the concrete *components* a caller will reach for — utilities, services, methods, functions, fields — and pin down their inputs, outputs, and error behavior, so that callers can use the software without reading the generated code. For example, a spec can require:
|
|
421
|
+
|
|
422
|
+
```plain
|
|
423
|
+
- Implement :CsvToJsonConverter: as a stateless utility component exposing two operations.
|
|
424
|
+
- :CsvToJson: takes a CSV row and a list of header strings, returns a JSON object keyed by header name. Infer and convert value types. Empty CSV values must be converted to null. Null values must be preserved — keys with null values must appear in the result, not be omitted. Handle CSV escaping (quoted values, commas within quotes). Raise an error if the number of columns does not match the headers.
|
|
425
|
+
- :JsonToCsv: takes a JSON object and a list of header strings defining column order, returns a CSV row without a header row. Handle CSV escaping for values containing commas or quotes. Output an empty string for null or missing fields. Extra keys in the JSON object that are not present in the headers list should be silently ignored.
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
This rule is complementary to the earlier "specs should be language-agnostic" guideline, not in conflict with it. Component names (`:CsvToJsonConverter:`, `:CsvToJson:`, `:JsonToCsv:`) and behavioral contracts belong in functional specs because they survive a language switch unchanged. Language-specific realizations of those contracts — "POJO class with static methods", "Python module with module-level functions", `@staticmethod`, `class Foo`, exception types like `IllegalArgumentException` vs `ValueError`, choice of test framework (`pytest` vs `JUnit`), mocking library, fixture style, assertion syntax — belong in `***implementation reqs***` and `***test reqs***`, because those are exactly what changes when the target language changes. Use `***implementation reqs***` for *how the production code is realized* (language, frameworks, libraries, syntax, error types) and `***test reqs***` for *how the tests are realized* (test framework, test runner, mocking and fixture conventions, parametrization style, naming conventions, file layout). The goal is that swapping languages requires editing only `***implementation reqs***` and `***test reqs***`; the functional spec for `:CsvToJsonConverter:` should read identically whether the project is in Python, Java, or anything else.
|
|
429
|
+
|
|
430
|
+
## Line Length Rule
|
|
431
|
+
|
|
432
|
+
**Keep every line in the `.plain` short.** When a sentence is too long, **do not** soft-wrap it across continuation lines — ***plain syntax requires every line inside a section to be its own list item starting with `- ` (with the possibility for nested bullet items). Instead, break the sentence down into multiple bullet items, each on its own line and each prefixed with `- `, nested under the parent bullet so the meaning stays grouped.
|
|
433
|
+
|
|
434
|
+
This rule applies to **every** spec update and to **all** sections — `***definitions***`, `***implementation reqs***`, `***test reqs***`, `***functional specs***`, `***acceptance tests***`, and concept explanations alike.
|
|
435
|
+
|
|
436
|
+
BAD — line is too long:
|
|
437
|
+
|
|
438
|
+
```plain
|
|
439
|
+
- :GatewayWebhook: should hand off :StripeRequest: to :StripeIntegration:.handle(), which returns a list of :EventEnvelope: dicts conforming to the gateway's contract.
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
WRONG SYNTAX AND BAD (AVOID AT ALL COSTS) — bare indented continuation lines without a leading `- ` are invalid ***plain syntax:
|
|
443
|
+
|
|
444
|
+
```plain
|
|
445
|
+
- :GatewayWebhook: should hand off :StripeRequest: to :StripeIntegration:.handle(),
|
|
446
|
+
which returns a list of :EventEnvelope: dicts conforming to the gateway's
|
|
447
|
+
contract.
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
GOOD — split at a natural clause boundary into nested `- ` bullets:
|
|
451
|
+
|
|
452
|
+
```plain
|
|
453
|
+
- :GatewayWebhook: should hand off :StripeRequest: to :StripeIntegration:.handle()
|
|
454
|
+
- The method returns a list of :EventEnvelope: dicts.
|
|
455
|
+
- The dicts must conform to the gateway's :EventEnvelope: contract.
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
If you find yourself writing a line longer than **120 characters**, stop and split it at a natural clause boundary into nested `- ` bullets (as in the GOOD example above) before moving on. Never use bare indented continuation lines without a leading `- ` — that is invalid ***plain syntax.
|
|
459
|
+
|
|
460
|
+
Do not paste long URLs, schema fragments, or example payloads inline either — those belong in `resources/` per the [Linked Resources](#linked-resources) rule above.
|
|
461
|
+
|
|
462
|
+
## Conflicting Specs and Conformance Test Debugging
|
|
463
|
+
|
|
464
|
+
The renderer can detect conflicting specs. Two functional specs may be in conflict if conformance tests for a previously passing spec begin to fail after a new spec is rendered. When a conformance test failure occurs, the first step is to determine **where the issue lies**. There are three possible outcomes:
|
|
465
|
+
|
|
466
|
+
1. **The implementation is incorrect** — the generated code does not correctly implement the functional spec. Fix the spec to clarify intent and re-render.
|
|
467
|
+
2. **The conformance tests are incorrect** — the generated tests do not accurately verify the spec. Adjust `***test reqs***` or `***acceptance tests***` to guide better test generation and re-render.
|
|
468
|
+
3. **The requirements conflict** — the two functional specs are inherently contradictory. One or both specs must be revised to resolve the conflict before re-rendering.
|
|
469
|
+
|
|
470
|
+
Conflicting specs are the most costly outcome and should be **prevented proactively**. When writing or modifying functional specs, carefully consider how each spec interacts with all previous specs. If ambiguity exists, add explicit detail to the spec to eliminate any possible interpretation that could conflict with earlier specs.
|
|
471
|
+
|
|
472
|
+
## Working with Specs
|
|
473
|
+
|
|
474
|
+
- The `.plain` files are the source of truth. Modify specs to change behavior, then re-render.
|
|
475
|
+
- The `resources/` directory contains schemas, API specs, transforms, and test fixtures referenced by the specs.
|
|
476
|
+
- Generated code in `plain_modules/` should not be manually edited — changes will be overwritten on the next render.
|
|
477
|
+
|
|
478
|
+
## Read-Only Generated Artifacts
|
|
479
|
+
|
|
480
|
+
All code in `plain_modules/` and `conformance_tests/` is generated and **must never be modified directly** — not the implementation code, not the unit tests, not the conformance tests. These artifacts can only be:
|
|
481
|
+
|
|
482
|
+
- **Read** — to understand what the generated code does, inspect behavior, and identify ambiguities in the specs.
|
|
483
|
+
- **Tested** — unit tests and conformance tests can be executed to verify correctness.
|
|
484
|
+
- **Debugged** — test failures and unexpected behavior should be traced through the generated code to understand root causes, but fixes must always be applied in the `.plain` specs, never in the generated code.
|
|
485
|
+
|
|
486
|
+
Each module has its own folder under `plain_modules/<module_name>/` containing the generated project (implementation + unit tests). Each module also has its own folder under `conformance_tests/<module_name>/`, with individual subfolders per functionality for conformance tests. Conformance tests may also include generated `***acceptance tests***` — these are equally read-only and serve the same purpose: gathering information and debugging the specs.
|
|
487
|
+
|
|
488
|
+
To change the generated code, **only the corresponding `.plain` spec files may be edited**:
|
|
489
|
+
- To change implementation or unit tests → modify the `***functional specs***`, `***implementation reqs***` or `***definitions***` sections of the spec.
|
|
490
|
+
- To guide conformance test generation → modify the `***test reqs***` section of the spec.
|
|
491
|
+
- To guide acceptance test generation → modify the `***acceptance tests***` subsections under functional specs.
|
|
492
|
+
|
|
493
|
+
The `test_scripts/` folder contains shell scripts for running unit tests and conformance tests against the generated code. These scripts are the entry point for test execution — see the [Running Tests](#running-tests) section for usage.
|
|
494
|
+
|
|
495
|
+
The workflow is: read the generated code to understand what it does, identify what is ambiguous or incorrect in the specs, then make changes exclusively in the `.plain` files and re-render.
|
|
496
|
+
|
|
497
|
+
## Common mistakes
|
|
498
|
+
|
|
499
|
+
- Usage of concepts before defining them
|
|
500
|
+
|
|
501
|
+
BAD
|
|
502
|
+
```***plain
|
|
503
|
+
***functional specs***
|
|
504
|
+
|
|
505
|
+
- Implement :Message:
|
|
506
|
+
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
GOOD
|
|
510
|
+
```***plain
|
|
511
|
+
***definitions***
|
|
512
|
+
|
|
513
|
+
- :Message: is an interface of communication between two users.
|
|
514
|
+
|
|
515
|
+
***functional specs***
|
|
516
|
+
|
|
517
|
+
- Implement :Message:
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
- Cyclic definitons
|
|
521
|
+
|
|
522
|
+
BAD
|
|
523
|
+
```***plain
|
|
524
|
+
***definitions***
|
|
525
|
+
|
|
526
|
+
- :Message: has an :Author:
|
|
527
|
+
|
|
528
|
+
- :Author: can create a :Message:
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
GOOD
|
|
532
|
+
```***plain
|
|
533
|
+
***definitions***
|
|
534
|
+
|
|
535
|
+
- :Message: is an interface of communication between two users.
|
|
536
|
+
|
|
537
|
+
- :Author: can create a :Message:
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
- Conflicting implementation requirements
|
|
541
|
+
|
|
542
|
+
BAD — both reqs in the same module
|
|
543
|
+
|
|
544
|
+
```***plain
|
|
545
|
+
***implementation reqs***
|
|
546
|
+
|
|
547
|
+
- :Implementation: should be in python
|
|
548
|
+
|
|
549
|
+
- :Implementation: should be in react
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
GOOD — split into two independent root modules
|
|
553
|
+
|
|
554
|
+
`backend.plain`
|
|
555
|
+
```***plain
|
|
556
|
+
***implementation reqs***
|
|
557
|
+
|
|
558
|
+
- :Implementation: should be in python
|
|
559
|
+
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
`frontend.plain`
|
|
563
|
+
```***plain
|
|
564
|
+
***implementation reqs***
|
|
565
|
+
|
|
566
|
+
- :Implementation: should be in react
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
## `codeplain` CLI reference
|
|
571
|
+
|
|
572
|
+
```txt
|
|
573
|
+
Render ***plain specs to target code.
|
|
574
|
+
|
|
575
|
+
positional arguments:
|
|
576
|
+
filename Path to the plain file to render. The directory containing this file has highest precedence for template loading, so
|
|
577
|
+
you can place custom templates here to override the defaults. See --template-dir for more details about template
|
|
578
|
+
loading.
|
|
579
|
+
|
|
580
|
+
options:
|
|
581
|
+
-h, --help show this help message and exit
|
|
582
|
+
--verbose, -v Enable verbose output
|
|
583
|
+
--base-folder BASE_FOLDER
|
|
584
|
+
Base folder for the build files
|
|
585
|
+
--build-folder BUILD_FOLDER
|
|
586
|
+
Folder for build files
|
|
587
|
+
--log-to-file, --no-log-to-file
|
|
588
|
+
Enable logging to a file. Defaults to True. Set to False to disable.
|
|
589
|
+
--log-file-name LOG_FILE_NAME
|
|
590
|
+
Name of the log file. Defaults to 'codeplain.log'.Always resolved relative to the plain file directory.If file on
|
|
591
|
+
this path already exists, the already existing log file will be overwritten by the current logs.
|
|
592
|
+
--render-range RENDER_RANGE
|
|
593
|
+
Specify a range of functionalities to render (e.g. `1` , `2`, `3`). Use comma to separate start and end IDs. If only
|
|
594
|
+
one functionality ID is provided, only that functionality is rendered. Range is inclusive of both start and end IDs.
|
|
595
|
+
--render-from RENDER_FROM
|
|
596
|
+
Continue generation starting from this specific functionality (e.g. `2`). The functionality with this ID will be
|
|
597
|
+
included in the output. The functionality ID must match one of the functionalities in your plain file.
|
|
598
|
+
--force-render Force re-render of all the required modules.
|
|
599
|
+
--unittests-script UNITTESTS_SCRIPT
|
|
600
|
+
Shell script to run unit tests on generated code. Receives the build folder path as its first argument (default:
|
|
601
|
+
'plain_modules').
|
|
602
|
+
--conformance-tests-folder CONFORMANCE_TESTS_FOLDER
|
|
603
|
+
Folder for conformance test files
|
|
604
|
+
--conformance-tests-script CONFORMANCE_TESTS_SCRIPT
|
|
605
|
+
Path to conformance tests shell script. Every conformance test script should accept two arguments: 1) Path to a
|
|
606
|
+
folder (e.g. `plain_modules/module_name`) containing generated source code, 2) Path to a subfolder of the conformance
|
|
607
|
+
tests folder (e.g. `conformance_tests/subfoldername`) containing test files.
|
|
608
|
+
--prepare-environment-script PREPARE_ENVIRONMENT_SCRIPT
|
|
609
|
+
Path to a shell script that prepares the testing environment. The script should accept the source code folder path as
|
|
610
|
+
its first argument.
|
|
611
|
+
--test-script-timeout TEST_SCRIPT_TIMEOUT
|
|
612
|
+
Timeout for test scripts in seconds. If not provided, the default timeout of 120 seconds is used.
|
|
613
|
+
--api [API] Alternative base URL for the API. Default: `https://api.codeplain.ai`
|
|
614
|
+
--api-key API_KEY API key used to access the API. If not provided, the `CODEPLAIN_API_KEY` environment variable is used.
|
|
615
|
+
--full-plain Full preview ***plain specification before code generation.Use when you want to preview context of all ***plain
|
|
616
|
+
primitives that are going to be included in order to render the given module.
|
|
617
|
+
--dry-run Dry run preview of the code generation (without actually making any changes).
|
|
618
|
+
--replay-with REPLAY_WITH
|
|
619
|
+
--template-dir TEMPLATE_DIR
|
|
620
|
+
Path to a custom template directory. Templates are searched in the following order: 1) Directory containing the plain
|
|
621
|
+
file, 2) Custom template directory (if provided through this argument), 3) Built-in standard_template_library
|
|
622
|
+
directory
|
|
623
|
+
--copy-build If set, copy the rendered contents of code in `--base-folder` folder to `--build-dest` folder after successful
|
|
624
|
+
rendering.
|
|
625
|
+
--build-dest BUILD_DEST
|
|
626
|
+
Target folder to copy rendered contents of code to (used only if --copy-build is set).
|
|
627
|
+
--copy-conformance-tests
|
|
628
|
+
If set, copy the conformance tests of code in `--conformance-tests-folder` folder to `--conformance-tests-dest`
|
|
629
|
+
folder successful rendering. Requires --conformance-tests-script.
|
|
630
|
+
--conformance-tests-dest CONFORMANCE_TESTS_DEST
|
|
631
|
+
Target folder to copy conformance tests of code to (used only if --copy-conformance-tests is set).
|
|
632
|
+
--render-machine-graph
|
|
633
|
+
If set, render the state machine graph.
|
|
634
|
+
--logging-config-path LOGGING_CONFIG_PATH
|
|
635
|
+
Path to the logging configuration file.
|
|
636
|
+
--headless Run in headless mode: no TUI, no terminal output except a single render-started message. All logs are written to the
|
|
637
|
+
log file.
|
|
638
|
+
|
|
639
|
+
configuration:
|
|
640
|
+
--config-name CONFIG_NAME
|
|
641
|
+
Name of the config file to look for. Looked up in the plain file directory and the current working directory.
|
|
642
|
+
Defaults to config.yaml.
|
|
643
|
+
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
---
|