PLAIN_REFERENCE.md
Project Overview
This repository is a workspace for writing and managing
***plain (codeplain) specifications. ***plain is a specification-driven language powered by AI that generates production-ready code from
spec files.
The
files in this repository 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.
***plain Language Reference
***plain is a specification language designed for writing software requirements in a clear, structured format. It generates production-ready code from
spec files using AI. Full documentation:
https://plainlang.org/docs/language-guide/
.plain File Structure
A
file consists of an optional YAML frontmatter section followed by standardized sections marked with
headers. There are four types of specification sections:
- — declares concepts used throughout the specification
***implementation reqs***
— non-functional requirements about how the software should be built
- — requirements for conformance testing
- — describes what the software should do
Every plain source file requires at least one functional spec and an associated implementation req. Functional specs must reside in leaf sections while other specifications can be placed also in non-leaf sections. Specifications in non-leaf sections apply to all of their subsections.
Concept Notation
Concepts are the building blocks of ***plain specifications. They are written between colons:
. Valid characters include letters, digits, plus, minus, dot, and underscore.
Concepts must be defined in
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 (directly or indirectly).
Example:
plain
***definitions***
- :User: is the user of :App:
- :Task: describes an activity that needs to be done by :User:. :Task: has:
- Name - a short description (required)
- Notes - additional details (optional)
- Due Date - completion deadline (optional)
- :TaskList: is a list of :Task: items.
- Initially :TaskList: should be empty.
Predefined Concepts
***plain provides predefined concepts available in all specifications without needing to be defined:
| Concept | Meaning |
|---|
| Content of the section |
:plainImplementationReqs:
| Content of the ***implementation reqs***
section |
| Content of the section |
| Content of the section |
| The system implementing |
:plainImplementationCode:
| The generated implementation code |
| Auto-generated unit tests for individual functionalities - their usage goes in in the implementation reqs section |
| Auto-generated tests that verify implementation conforms to specs |
| / | Tests that validate specific aspects of the implementation |
Definitions Section
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
section, from an
ed module's definitions, or from a
d module's
. Attributes and constraints can be nested as sub-bullets.
plain
***definitions***
- :ConceptName: is a description of the concept.
- Additional details or attributes can be nested
- Multiple attributes can be listed
Implementation Reqs Section
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.
plain
***implementation reqs***
- :Implementation: should be in Python.
- :MainExecutableFile: of :App: should be called "hello_world.py".
- :Implementation: should include :Unittests: using Unittest framework!
Test Reqs Section
Specifies requirements for conformance testing — test frameworks, execution methods, and testing constraints. Only used when writing and fixing conformance tests (not unit tests).
plain
***test reqs***
- :ConformanceTests: of :App: should be implemented in Python using Unittest framework.
- :ConformanceTests: will be run using "python -m unittest discover" command.
- :ConformanceTests: must be implemented and executed - do not use unittest.skip().
Functional Specs Section
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.
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.
Functional specs are in
chronological order — earlier specs are rendered before later ones. Functional specs defined in
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.
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.
plain
***functional specs***
- Implement the entry point for :App:.
- Show :TaskList:.
- :User: should be able to add :Task:. Only valid :Task: items can be added.
- :User: should be able to delete :Task:.
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.
plain
***functional specs***
- :User: should be able to send a :Message: to a :Conversation:.
- A :Message: must have non-empty content.
- The :Message: is appended to the end of the :Conversation:.
- All :Participant: members of the :Conversation: can see the new :Message:.
Acceptance Tests
Nested under individual functional specs to specify how to verify correct implementation. They extend conformance tests and are implemented according to the
specification.
plain
***functional specs***
- Display "hello, world"
***acceptance tests***
- :App: should exit with status code 0 indicating successful execution.
- :App: should complete execution in under 1 second.
YAML Frontmatter
The frontmatter is enclosed between
markers and supports:
-
— includes definitions, implementation reqs, and test reqs from templates. Imported modules must not contain functional specs. The default import directory is
— the
prefix is not needed (e.g.,
resolves to
).
-
— specifies dependencies on other root-level modules that must be built first. Unlike
, required modules can contain functional specs and represent complete software modules. Requires paths point to root-level modules (e.g.,
,
).
-
— optional description of the specification.
-
— concepts that must be defined by any module that imports this spec.
-
— concepts made available to modules that
this one.
Exports are not transitive. A concept exported from module
is visible only to the modules that
directly. If module
and module
, the concepts
exports are
not automatically visible to
— only the concepts
itself re-exports are. To pass a concept further down the chain, the intermediate module must re-declare it in its own
list (and define / forward it in its own
as needed). This applies at every hop: each module is responsible for explicitly exporting whatever it wants its own
-ers to see.
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:
- Create an import module under (e.g.
template/shared_domain.plain
) and put the concept's entry there. Import modules carry definitions, implementation reqs, and test reqs only — never functional specs.
- In every module that needs the concept (no matter how deep in the chain), add the import module to its frontmatter list. The concept is then visible in that module directly, without any plumbing.
- None of the -chained modules need to re-export the concept anymore — each one imports what it actually uses.
Use the
skill to scaffold this, and
when you discover the same concept has been scattered across several modules and needs to be pulled back into a single shared import.
Rule of thumb: if a concept crosses
one hop,
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.
Linked Resources
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
file location. Only files in the same folder (and subfolders) are supported; no external URLs.
plain
- :User: should be able to add :Task:. The details of the user interface
are provided in the file [task_modal_specification.yaml](task_modal_specification.yaml).
Structured protocol artifacts must be linked resources, never transcribed into prose. Anything that has a formal machine-readable shape — JSON Schema, OpenAPI / Swagger specs, GraphQL SDL, Protobuf / gRPC
files, Avro / Thrift schemas, XML XSDs, AsyncAPI specs, JSON-RPC method definitions, wire-protocol descriptions, payload examples, etc. — belongs in a file under
(or a subfolder of the
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:
- 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.
- 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.
- 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 sections.
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 rule below.
plain
***definitions***
- :TaskCreateRequest: is the JSON payload for creating a task, defined by
[resources/task_create_request.schema.json](resources/task_create_request.schema.json).
- :TasksAPI: is the public HTTP surface for tasks, defined by
[resources/tasks_openapi.yaml](resources/tasks_openapi.yaml).
***functional specs***
- :User: should be able to add :Task: by POSTing :TaskCreateRequest: to the
`POST /tasks` endpoint of :TasksAPI:. The endpoint responds per :TasksAPI:.
Each linked resource must be referenced from exactly one place in the project — a single functional spec, implementation requirement, or
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.
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:
- Define a concept under whose explanation links the resource exactly once.
- Use the concept token () wherever the resource was previously inlined.
For example, instead of linking
task_modal_specification.yaml
from two different functional specs:
plain
***definitions***
- :TaskModalSpec: is the user-interface contract for the task modal,
fully described in [task_modal_specification.yaml](task_modal_specification.yaml).
***functional specs***
- :User: should be able to add :Task: using :TaskModalSpec:.
- :User: should be able to edit :Task: using :TaskModalSpec:.
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
link a second time,
stop — create the concept first.
Template System
***plain supports template inclusion using
syntax:
plain
{% include "python-console-app-template.plain", main_executable_file_name: "my_app.py" %}
Parameters are passed as key-value pairs. Inside the template, they are accessed using variable syntax (
). Only variables are supported — conditionals, loops, and other Liquid features are not available.
Comments
Lines starting with
are ignored when rendering:
plain
> This is a comment in ***plain
Best Practices
- Reference concepts consistently — use notation to disambiguate key concepts
- Keep it simple — specs should be readable by both humans and AI
- Leverage templates — use the standard template library for common patterns
- Use acceptance tests — add them for requirements that need verification
- Be specific — write clear, testable requirements in functional specs
- Define before use — always define concepts in before referencing them
- Start with imports — import relevant templates before defining your own concepts
Repository Structure
*.plain # Specification files (the source of truth)
template/*.plain # Reusable template specs imported by module specs
plain_modules/ # Generated code output (one folder per .plain spec)
resources/ # Schemas, API specs, transforms, test fixtures
conformance_tests/ # Generated conformance tests (one folder per module)
test_scripts/ # Scripts for running unit and conformance tests
config.yaml # Codeplain configuration
Generated artifacts (gitignored):
plain_modules/<module_name>/
— generated project for each spec (implementation + unit tests)
conformance_tests/<module_name>/
— generated conformance tests for each module
How Modules Work
There are two types of modules:
Import Modules
An import module lives in the
directory and contains
only ,
***implementation reqs***
, and/or
. It must
not contain
and must
not use
. It may optionally
other templates for layered reuse.
When a module
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 is
, so the
prefix is not needed (e.g.,
).
Requires Modules
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
relationship ensures the build order is correct.
- The required module's generated code (
plain_modules/<required_module>
) is copied as the starting point.
- The required module's become visible as previous functional specs.
- Only from the required module are available (not its full definitions).
A module can use both
and
together.
points to other root-level modules (e.g.,
,
);
resolves from the default
directory without needing the prefix (e.g.,
). Modules with functional specs live at the repository root. Import modules (templates) live in
.
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
when they target the same language, framework, and runtime. A runtime/network dependency between systems is
not a reason to use
. For example, a React frontend that talks to a Python/FastAPI backend over HTTP must
not — the stacks differ. Model that pair as two independent root modules (each with its own
and test scripts), and express the contract between them through a shared API schema in
or shared concepts in an
ed template, not through
.
Contracts Between Modules
Modules can use
and
to enforce contracts between them. A template declaring
means any module that imports it must define those concepts. A module declaring
controls which concepts are visible to modules that
it.
Exported concepts are not transitive. If module A exports a concept and module B
A, module B can use that concept — but if module C
B, it does
not automatically gain access to A's exported concepts. If a concept needs to be shared across multiple
modules, define it in a common import module and have each module
that shared template.
Running Tests
Test scripts live in
and are run from the repo root:
bash
# Run all unit tests for a module
./test_scripts/run_unittests.sh <module_name>
# Run a single unit test
./test_scripts/run_unittests_single.sh <module_name>
# Run conformance tests
./test_scripts/run_conformance_tests.sh <module_name> <conformance_tests_folder>
Testing Scripts
Every ***plain project ships shell scripts under
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.
Why these scripts exist (and why they're shaped the way they are)
The
primary purpose of these scripts is
automated execution by the renderer, not manual invocation by a developer. The user
can run them by hand (see
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:
- Conformance tests are per-functional-spec. Each functional spec in a module has its own folder under
conformance_tests/<module>/<spec>/
. After the renderer finishes generating code for a new functional spec, it runs the conformance tests of every previous functional spec in the same module to detect regressions — see Conformance Test Workflow. For a module 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.
- 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. That's the right design — it keeps each invocation hermetic and lets the renderer reorder or skip specs without breaking anything.
- 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 + , Maven dependency tree, , , ...) on every one of those N invocations. That's of wasted work for every render.
- 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 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 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.
- 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 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 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 -equivalent for it.
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.
The three scripts
- / — runs the auto-generated unit tests in . Authored by the
implement-unit-testing-script
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
, , , , etc.) and never relies on any other script having run first.
run_conformance_tests_<lang>.sh
/ — runs the conformance tests in conformance_tests/<module>/<spec>/
against the generated implementation. Authored by the implement-conformance-testing-script
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.
prepare_environment_<lang>.sh
/ — 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 so the N subsequent conformance invocations can attach to the warmed environment instead of re-installing. Authored by the implement-prepare-environment-script
skill. Receives one positional argument: the source build folder name. It does not feed the unit-test script — see is conformance-only below.
is conformance-only (common mistake)
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:
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 (
) is
completely independent of it — it does not read from
's working folder, does not require
to have run, and must install whatever dependencies it needs on its own.
Why:
- Unit tests run against , conformance tests run against . The two scripts stage into different places. populates for conformance; the unit-test script does its own staging into its own working folder and installs its own dependencies there.
- 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 depended on having run, those one-off unit-test invocations would silently fail (or be "fixed" by a misguided edit to make it depend on ).
- The skill contract enforces it.
implement-unit-testing-script
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.
If you find yourself authoring (or asked to author) a
script that handles unit-test dependencies too,
stop. The unit-test script handles its own dependencies. Adding a unit-test path into
couples scripts that should stay independent, and breaks the activate-only contract between
and
.
Shared rules across all three scripts
Anything not listed here is documented in the individual skill file:
- 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 , 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 is an input folder, or whose target path starts with the input folder, stop — the write has to go into .
- Shell flavor matches the host. on macOS / Linux, on Windows. A project intended for both OSes ships both files in matching pairs ( + for each language must agree on working-folder name and isolation paths).
- Exit codes are part of the contract. for unrecoverable errors (missing toolchain, bad args, can't enter the working folder, install failed); 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 and — branch on these codes.
- Wired in via . Each script that is actually generated must be referenced from the relevant via the ,
conformance-tests-script:
, and prepare-environment-script:
keys respectively. See the skill for the canonical assembly. If prepare-environment-script
is declared, must be declared too — a prepare script only makes sense in service of conformance, and will hard-fail a project that violates this.
- 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 ). 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.
- Dependency isolation is project-local. Each language's package cache / virtual env / build repo lives inside the working folder ( for Python, for Node, for Java, for Go, for Rust, 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.
- No language-package checks live in these scripts. The scripts themselves install language packages via
pip install -r requirements.txt
, , mvn -Dmaven.repo.local=...
, , , 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 , not in these scripts.
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.
Writing Functional Specs
-
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.
-
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.
-
Specs should be language-agnostic. Avoid using programming language-specific terminology (e.g., generics syntax, framework annotations, language-specific collection types) 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.
-
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:
,
***implementation reqs***
,
, and
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.
-
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:
- How to run the built software — the entry-point command (e.g. , , ), 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.
- 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.
Concretely, a reader should never have to open
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.
-
Encapsulate functionality in functional specs. 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.
Working with Specs
- The files are the source of truth. Modify specs to change behavior, then re-render.
- The directory contains reusable template specs that define common patterns.
- The directory contains schemas, API specs, transforms, and test fixtures referenced by the specs.
- Generated code in should not be manually edited — changes will be overwritten on the next render.
Read-Only Generated Artifacts
All code in
and
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:
- Read — to understand what the generated code does, inspect behavior, and identify ambiguities in the specs.
- Tested — unit tests and conformance tests can be executed to verify correctness.
- 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 specs, never in the generated code.
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
— these are equally read-only and serve the same purpose: gathering information and debugging the specs.
To change the generated code,
only the corresponding spec files may be edited:
- To change implementation or unit tests → modify the ,
***implementation reqs***
or sections of the spec.
- To guide conformance test generation → modify the section of the spec.
- To guide acceptance test generation → modify the subsections under functional specs.
The
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 section for usage.
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
files and re-render.
Conformance Test Workflow
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.
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.
Conflicting Specs and Conformance Test Debugging
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:
- The implementation is incorrect — the generated code does not correctly implement the functional spec. Fix the spec to clarify intent and re-render.
- The conformance tests are incorrect — the generated tests do not accurately verify the spec. Adjust or to guide better test generation and re-render.
- The requirements conflict — the two functional specs are inherently contradictory. One or both specs must be revised to resolve the conflict before re-rendering.
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.
Common mistakes
- Usage of concepts before defining them
BAD
***functional specs***
- Implement :Message:
GOOD
***definitions***
- :Message: is an interface of communication between two users.
***functional specs***
- Implement :Message:
BAD
***definitions***
- :Message: has an :Author:
- :Author: can create a :Message:
GOOD
***definitions***
- :Message: is an interface of communication between two users.
- :Author: can create a :Message:
- Conflicting implementation requirements
BAD — both reqs in the same module
***implementation reqs***
- :Implementation: should be in python
- :Implementation: should be in react
GOOD — split into two independent root modules
***implementation reqs***
- :Implementation: should be in python
***implementation reqs***
- :Implementation: should be in react