plain-forge 1.0.2 → 1.0.3
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/bin/cli.mjs +86 -3
- package/forge/rules/definitions.md +16 -0
- package/forge/rules/func-specs.md +8 -1
- package/forge/rules/impl-reqs.md +8 -1
- package/forge/rules/integration-embedded.md +149 -0
- package/forge/rules/integration-standalone.md +134 -0
- package/forge/rules/integrations.md +91 -0
- package/forge/rules/line-length.md +46 -0
- package/forge/rules/linked-resources.md +66 -0
- package/forge/rules/requires-modules.md +8 -2
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -17,6 +17,39 @@ const AGENTS = {
|
|
|
17
17
|
};
|
|
18
18
|
const SCOPES = ["project", "global"];
|
|
19
19
|
|
|
20
|
+
const BANNER = `\x1b[38;2;224;255;110m██████╗ ██╗ █████╗ ██╗███╗ ██╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
21
|
+
██╔══██╗██║ ██╔══██╗██║████╗ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
|
|
22
|
+
██████╔╝██║ ███████║██║██╔██╗ ██║█████╗█████╗ ██║ ██║██████╔╝██║ ███╗█████╗
|
|
23
|
+
██╔═══╝ ██║ ██╔══██║██║██║╚██╗██║╚════╝██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
|
|
24
|
+
██║ ███████╗██║ ██║██║██║ ╚████║ ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
|
|
25
|
+
╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝\x1b[0m
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const TAGLINE = "turn ideas into ***plain specs.";
|
|
29
|
+
|
|
30
|
+
function stripAnsi(s) {
|
|
31
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function bannerWidth() {
|
|
35
|
+
return stripAnsi(BANNER)
|
|
36
|
+
.split("\n")
|
|
37
|
+
.reduce((max, line) => Math.max(max, line.length), 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printBanner() {
|
|
41
|
+
if (!process.stdout.isTTY) return;
|
|
42
|
+
process.stdout.write("\n" + BANNER + "\n");
|
|
43
|
+
|
|
44
|
+
const width = bannerWidth();
|
|
45
|
+
const pad = Math.max(0, Math.floor((width - TAGLINE.length) / 2));
|
|
46
|
+
const tag = TAGLINE.replace(
|
|
47
|
+
"***plain",
|
|
48
|
+
"\x1b[38;2;121;252;150m***plain\x1b[0m",
|
|
49
|
+
);
|
|
50
|
+
process.stdout.write(" ".repeat(pad) + tag + "\n\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
20
53
|
function usage() {
|
|
21
54
|
console.log(`Usage: plain-forge install [options]
|
|
22
55
|
|
|
@@ -90,7 +123,7 @@ function promptChoice(question, choices) {
|
|
|
90
123
|
render();
|
|
91
124
|
} else if (key.name === "return") {
|
|
92
125
|
cleanup();
|
|
93
|
-
output.write(` \x1b[32m${choices[index]}\x1b[0m\n`);
|
|
126
|
+
output.write(` \x1b[32m${choices[index]}\x1b[0m\n\n`);
|
|
94
127
|
resolve(choices[index]);
|
|
95
128
|
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
96
129
|
cleanup();
|
|
@@ -126,8 +159,10 @@ function copyTree(srcDir, destDir) {
|
|
|
126
159
|
}
|
|
127
160
|
|
|
128
161
|
async function cmdInstall(args) {
|
|
162
|
+
printBanner();
|
|
163
|
+
|
|
129
164
|
let agent = args.agent;
|
|
130
|
-
if (!agent) agent = await promptChoice("Which agent?", Object.keys(AGENTS));
|
|
165
|
+
if (!agent) agent = await promptChoice("Which agent ?", Object.keys(AGENTS));
|
|
131
166
|
if (!Object.hasOwn(AGENTS, agent)) {
|
|
132
167
|
console.error(
|
|
133
168
|
`unknown agent "${agent}". valid: ${Object.keys(AGENTS).join(", ")}`,
|
|
@@ -136,7 +171,7 @@ async function cmdInstall(args) {
|
|
|
136
171
|
}
|
|
137
172
|
|
|
138
173
|
let scope = args.scope;
|
|
139
|
-
if (!scope) scope = await promptChoice("Scope?", SCOPES);
|
|
174
|
+
if (!scope) scope = await promptChoice("Scope ?", SCOPES);
|
|
140
175
|
if (!SCOPES.includes(scope)) {
|
|
141
176
|
console.error(`unknown scope "${scope}". valid: ${SCOPES.join(", ")}`);
|
|
142
177
|
process.exit(2);
|
|
@@ -162,6 +197,54 @@ async function cmdInstall(args) {
|
|
|
162
197
|
console.log(` skills: ${skillsCount}`);
|
|
163
198
|
console.log(` rules: ${rulesCount}`);
|
|
164
199
|
console.log(` docs: ${docsCount}`);
|
|
200
|
+
console.log();
|
|
201
|
+
printNextSteps(agent);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function printNextSteps(agent) {
|
|
205
|
+
const bold = (s) => `\x1b[1;97m${s}\x1b[0m`;
|
|
206
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
207
|
+
const plain = (s) => `\x1b[38;2;121;252;150m${s}\x1b[0m`;
|
|
208
|
+
const codeplain = (s) => `\x1b[38;2;224;255;110m${s}\x1b[0m`;
|
|
209
|
+
const link = (s) => `\x1b[4;38;2;95;175;255m${s}\x1b[0m`;
|
|
210
|
+
|
|
211
|
+
console.log(`\x1b[1mnext steps:\x1b[0m`);
|
|
212
|
+
console.log(
|
|
213
|
+
` 1. open a project folder and start your ${agentLabel(agent)} session.`,
|
|
214
|
+
);
|
|
215
|
+
console.log(` 2. invoke one of:`);
|
|
216
|
+
console.log(
|
|
217
|
+
` ${bold("forge-plain")} — start a brand-new ${plain("***plain")} project from scratch`,
|
|
218
|
+
);
|
|
219
|
+
console.log(
|
|
220
|
+
` ${bold("init-plain-project")} — scaffold a minimal ${plain("***plain")} project to grow feature-by-feature`,
|
|
221
|
+
);
|
|
222
|
+
console.log(
|
|
223
|
+
` ${bold("add-feature")} — add a feature to an existing ${plain("***plain")} project`,
|
|
224
|
+
);
|
|
225
|
+
console.log();
|
|
226
|
+
console.log(
|
|
227
|
+
`prerequisite: install the ${codeplain("codeplain")} CLI to render your specs into code — ${link("https://www.codeplain.ai/")}`,
|
|
228
|
+
);
|
|
229
|
+
console.log(
|
|
230
|
+
`usage guide: ${link("https://github.com/Codeplain-ai/plain-forge#usage")}`,
|
|
231
|
+
);
|
|
232
|
+
console.log();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function agentLabel(agent) {
|
|
236
|
+
switch (agent) {
|
|
237
|
+
case "claude":
|
|
238
|
+
return "Claude Code";
|
|
239
|
+
case "codex":
|
|
240
|
+
return "Codex";
|
|
241
|
+
case "forgecode":
|
|
242
|
+
return "ForgeCode";
|
|
243
|
+
case "universal":
|
|
244
|
+
return "AI coding agent";
|
|
245
|
+
default:
|
|
246
|
+
return "agent";
|
|
247
|
+
}
|
|
165
248
|
}
|
|
166
249
|
|
|
167
250
|
async function main() {
|
|
@@ -16,6 +16,22 @@ When writing or editing a `***definitions***` section in a `.plain` file, always
|
|
|
16
16
|
- Concept names must be globally unique across the spec and all its imports
|
|
17
17
|
- Check for collisions with imported templates, `import` and `requires` modules before adding
|
|
18
18
|
|
|
19
|
+
## Predefined concepts (do not redefine)
|
|
20
|
+
- ***plain ships several predefined concepts that are available in every module without being defined
|
|
21
|
+
- Never add a `***definitions***` entry for any of them — the renderer treats redefinitions as conflicts
|
|
22
|
+
|
|
23
|
+
| Predefined concept | Meaning |
|
|
24
|
+
|--------------------|---------|
|
|
25
|
+
| `:plainDefinitions:` | Content of the `***definitions***` section |
|
|
26
|
+
| `:plainImplementationReqs:` | Content of the `***implementation reqs***` section |
|
|
27
|
+
| `:plainFunctionality:` | Content of the `***functional specs***` section |
|
|
28
|
+
| `:plainTestReqs:` | Content of the `***test reqs***` section |
|
|
29
|
+
| `:Implementation:` | The system implementing `:plainFunctionality:` |
|
|
30
|
+
| `:plainImplementationCode:` | The generated implementation code |
|
|
31
|
+
| `:UnitTests:` | Auto-generated unit tests for individual functionalities |
|
|
32
|
+
| `:ConformanceTests:` | Auto-generated tests verifying conformance to the spec |
|
|
33
|
+
| `:AcceptanceTest:` / `:AcceptanceTests:` | Tests validating specific aspects of the implementation |
|
|
34
|
+
|
|
19
35
|
## Define before use
|
|
20
36
|
- A concept must be defined before it is referenced in any section (definitions, implementation reqs, functional specs, test reqs)
|
|
21
37
|
- Sources of definitions: the module's own `***definitions***`, an `import`ed module's definitions, or a `require`d module's `exported_concepts`
|
|
@@ -26,7 +26,10 @@ When writing or editing a `***functional specs***` section in a `.plain` file, a
|
|
|
26
26
|
|
|
27
27
|
## Language agnosticism
|
|
28
28
|
- Write in terms of behavior, concepts, and domain logic
|
|
29
|
-
-
|
|
29
|
+
- Avoid language-specific terminology: generics syntax, framework annotations, language-specific collection types, decorator syntax, base-class keywords, framings like "POJO" or "dataclass"
|
|
30
|
+
- General technical terms that are not language-specific are fine: null values, JSON types, HTTP status codes, REST endpoints, etc.
|
|
31
|
+
- **Naming concrete components is encouraged.** Functional specs can and should refer to `:CsvToJsonConverter:` and its methods `:CsvToJson:` / `:JsonToCsv:` and pin down their inputs, outputs, and error behavior — those names are part of the public contract and survive a language switch. What they must **not** do is bake in how the contract is realized (`@staticmethod`, `class Foo extends Bar`, `List<T>`, etc.)
|
|
32
|
+
- **Litmus test:** if the project switched from Python to Java (or vice versa), would the functional spec read correctly with only `***implementation reqs***` updated? If yes, the spec is language-agnostic. If the spec itself would need rewording, the construct belongs in implementation reqs.
|
|
30
33
|
|
|
31
34
|
## Disambiguation
|
|
32
35
|
- Each functional spec must be unambiguous — the renderer should have only one reasonable interpretation
|
|
@@ -44,6 +47,10 @@ When writing or editing a `***functional specs***` section in a `.plain` file, a
|
|
|
44
47
|
- `requires` modules only receive functional specs — do not rely on implementation reqs to convey behavior
|
|
45
48
|
- Behavior that downstream modules need must be expressed in functional specs, not elsewhere
|
|
46
49
|
|
|
50
|
+
## Line length
|
|
51
|
+
- See [`line-length.md`](line-length.md) — applies to every section, but bites hardest here because functional specs trend long
|
|
52
|
+
- Hard limit: 120 characters. When a line gets too long, split at a natural clause boundary into nested `- ` bullets — **never** use bare indented continuation lines (invalid ***plain syntax)
|
|
53
|
+
|
|
47
54
|
## Acceptance tests
|
|
48
55
|
- Nest `***acceptance tests***` under a functional spec when verification criteria are needed
|
|
49
56
|
- Each acceptance test must be a **full workflow test** — a specific scenario that exercises the functional spec end-to-end, not a unit-level check of a single field or condition
|
package/forge/rules/impl-reqs.md
CHANGED
|
@@ -21,11 +21,18 @@ When writing or editing an `***implementation reqs***` section in a `.plain` fil
|
|
|
21
21
|
- Algorithm descriptions: specific approaches when behavior alone is insufficient
|
|
22
22
|
- Performance guidance: memory constraints, streaming requirements, batching strategies
|
|
23
23
|
- Language-specific constructs: generics, annotations, framework-specific types and idioms
|
|
24
|
+
- **Unit-test guidance: framework, structure, mocking conventions, file layout** — unit tests are part of the generated codebase, so requirements that shape them are implementation reqs
|
|
24
25
|
|
|
25
26
|
## What does NOT belong here
|
|
26
27
|
- Behavior and features → `***functional specs***`
|
|
27
28
|
- Concept definitions → `***definitions***`
|
|
28
|
-
- Conformance
|
|
29
|
+
- **Conformance-test guidance** → `***test reqs***`
|
|
30
|
+
- **Acceptance-test scenarios** → `***acceptance tests***` nested under the relevant functional spec
|
|
31
|
+
|
|
32
|
+
## Unit tests vs conformance tests (common mistake)
|
|
33
|
+
- Unit-test guidance (`:UnitTests:` framework, structure, mocking) goes **here**, not in `***test reqs***`
|
|
34
|
+
- `***test reqs***` is exclusively for `:ConformanceTests:` — framework, execution command, mocking policy, environment setup
|
|
35
|
+
- Putting unit-test guidance in `***test reqs***` is one of the most common authoring mistakes; the rendered code will silently miss those requirements because the unit-test generator only reads `***implementation reqs***`
|
|
29
36
|
|
|
30
37
|
## Encapsulation warning
|
|
31
38
|
- `requires` modules only receive functional specs from their dependencies — not implementation reqs
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for authoring ***plain specs for REST API integrations embedded into an existing host codebase
|
|
3
|
+
globs: "**/*.plain"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rules for **embedded** integration specs
|
|
7
|
+
|
|
8
|
+
When an integration `.plain` module is **embedded** — meaning the generated code in `plain_modules/` is consumed in-process by an existing host codebase as a library / module — these rules apply on top of the shared rules in [`integrations.md`](integrations.md). If anything below contradicts a guess made from memory, the rules here win.
|
|
9
|
+
|
|
10
|
+
Embedded means: the host codebase already exists, has its own language / framework / dependency manager / packaging layout, and the integration must conform to all of that without negotiation.
|
|
11
|
+
|
|
12
|
+
## The host codebase dictates the tech stack (hard rule)
|
|
13
|
+
|
|
14
|
+
- Language, framework, dependency manager, packaging layout, coding standards, error model, logging library, and architecture are **inherited** from the host — they are **never chosen** by the integration spec
|
|
15
|
+
- Do not re-ask the user about any of these in any phase — they are facts to be discovered from the host's manifest files (`pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`, `pom.xml`, …) and source tree
|
|
16
|
+
- If a Phase 3 (`forge-plain`) tech-stack question seems to push back on a host rule, treat the host as ground truth and rewrite the question
|
|
17
|
+
- Implementation reqs added in Phase 3 are **transcribed** from the host stack verbatim — host language and exact version, host framework + version, dependency manager and manifest path, packaging layout, host conventions the contract must follow, and every host-package version the contract pins
|
|
18
|
+
|
|
19
|
+
## Discover before you ask
|
|
20
|
+
|
|
21
|
+
Run host discovery **before** the first Phase 1 question. Treat the results as ground truth for everything that follows.
|
|
22
|
+
|
|
23
|
+
1. **Locate the host's `.plain` setup (if any).** Look for an existing `.plain` file or directory, a `config.yaml` declaring `plain_source_dir` / `plain_modules_dir` / `resources_dir` / `test_scripts_dir`, a `plain_modules/`, a `resources/`, and a `test_scripts/`. If the host already has a `.plain` setup, **adopt it verbatim**: the new integration module lands inside the existing `plain_source_dir` and `requires` the relevant base module (use `create-requires-module`). Do not create a parallel layout.
|
|
24
|
+
2. **Read every existing integration in the host.** For each one, extract: intent and scope, host base class / interface / protocol the integration subclasses or implements, package path and naming convention, configuration pattern (env vars, settings module, secret manager), error / exception hierarchy, logging / metrics / tracing conventions, testing pattern (live vs recorded vs mocked, fixture location, conformance-test layout). The new integration must follow the same patterns unless the user explicitly opts out.
|
|
25
|
+
3. **Capture findings in the `host-codebase` concept.** Cite the existing integration by file path so the reasoning is auditable. The concept holds, as facts:
|
|
26
|
+
- Host codebase root (absolute or project-relative path)
|
|
27
|
+
- Host language and exact version (from the manifest file)
|
|
28
|
+
- Dependency manager and manifest file path (pip + `requirements.txt`, Poetry, uv, npm, pnpm, yarn, Go modules, Cargo, Maven, Gradle, …)
|
|
29
|
+
- Package / module path inside the host where the integration will be consumed
|
|
30
|
+
- Fully qualified import path of any host class / interface / struct / protocol the integration must conform to (e.g. `host_project.integrations.base.IntegrationContract`)
|
|
31
|
+
- Host conventions (custom base classes, Pydantic major version, sync vs. async style, exception hierarchy, dependency-injection seams, logging library)
|
|
32
|
+
- Target generated-class fully qualified name under `plain_modules/` (e.g. `plain_modules.integrations.<provider>.Client`) and the host base class it should subclass
|
|
33
|
+
4. **Only ask the user what the codebase cannot tell you.** The user's time goes into the third-party API itself (provider, docs, endpoints, edge cases, webhooks) and authentication / credentials. Everything else is a deduction. If a deduction is ambiguous (two existing integrations subclass two different base classes), surface the ambiguity with a single-question `AskUserQuestion` that quotes both code locations — the question is about resolving a conflict the host already contains, not about asking the user to design the integration.
|
|
34
|
+
|
|
35
|
+
## Reference host symbols by fully qualified import path
|
|
36
|
+
|
|
37
|
+
- Every host class, interface, struct, exception, or type alias that appears in a spec must be written with its full dotted / slashed import path (e.g. `host_project.integrations.base.IntegrationContract`, `@host/integrations#Contract`) and tagged in the spec text as **"imported from the host codebase; do not redefine"**
|
|
38
|
+
- The renderer is allowed to redefine **only** symbols the host does not provide and the contract schema does not capture
|
|
39
|
+
- Naming the symbol by FQN is not optional decoration — it tells the renderer where the type comes from, which prevents a duplicate definition under `plain_modules/`
|
|
40
|
+
|
|
41
|
+
## Add host files as linked resources, never restate them
|
|
42
|
+
|
|
43
|
+
- Every host file the integration touches (base classes, configuration modules, registries, exception classes, lifecycle hooks) is added under `resources/host/` via the `add-resource` skill and referenced from the relevant spec using `***linked resource***` syntax
|
|
44
|
+
- **Never inline a host file's contents** into a spec
|
|
45
|
+
- **Never describe a host symbol's shape from memory** — the renderer reads the linked file's bytes and that is the source of truth
|
|
46
|
+
- This obeys the broader [`linked-resources.md`](linked-resources.md) rules — a directory is not a valid link, a URL is not a valid link, a binary is not a valid link
|
|
47
|
+
|
|
48
|
+
## The contract spec declares inheritance, not duplication
|
|
49
|
+
|
|
50
|
+
- The entry-point class / interface / struct in the contract spec must `subclass` / `implements` / `embeds` the host symbol by its full import path
|
|
51
|
+
- The spec describes only the **integration-specific additions and overrides** — never restates the parent's fields or methods
|
|
52
|
+
- The additions and overrides are themselves expressed in the linked schema under `resources/contract/` (JSON Schema or OpenAPI), with `allOf` / `$ref` extending the host's schema rather than duplicating fields
|
|
53
|
+
- If a host base class adds fields the integration shouldn't redeclare, the contract schema's `allOf` chain captures that explicitly
|
|
54
|
+
|
|
55
|
+
## Renderer directives go in the spec, shapes go in the schema
|
|
56
|
+
|
|
57
|
+
Each contract spec carries the language-specific glue that the schema can't express:
|
|
58
|
+
|
|
59
|
+
- Target generated-class fully qualified name (e.g. `plain_modules.integrations.<provider>.Client`)
|
|
60
|
+
- Target file path under `plain_modules/`
|
|
61
|
+
- Host base class import path to subclass / implement
|
|
62
|
+
- Host-package version pins (e.g. `pydantic ~= 2.5`, `fastapi ^0.110`)
|
|
63
|
+
- Framework-specific decorators or metaclasses (`model_config`, `@Depends`, …)
|
|
64
|
+
|
|
65
|
+
The renderer reads the directives from the spec and the shapes from the linked schema, then emits the host-language class. The spec must **not** also contain a class body or a field list — that creates two sources of truth and they will drift.
|
|
66
|
+
|
|
67
|
+
## Single source of truth for the host root
|
|
68
|
+
|
|
69
|
+
- The `host-codebase` concept holds the host root path as a **fact**
|
|
70
|
+
- Test scripts, `prepare_environment`, configuration-loading specs, and any other spec that needs the host location reads it from **that one fact** (via the env var declared in the configuration concept)
|
|
71
|
+
- Never hardcode the host path in any spec, script, or runtime config
|
|
72
|
+
|
|
73
|
+
## No host-overlapping reqs
|
|
74
|
+
|
|
75
|
+
- Implementation reqs added in any phase must not contradict the host codebase — same language, same dependency manager, same packaging layout, same error hierarchy, same logging library
|
|
76
|
+
- If two reqs are in tension (one from the host, one newly authored), the host wins; rewrite or drop the newly authored req
|
|
77
|
+
- Do not author a req that re-declares something the host already enforces — that's a maintenance burden with no benefit
|
|
78
|
+
|
|
79
|
+
## Test-script wiring — merge `plain_modules` into the host, run tests there
|
|
80
|
+
|
|
81
|
+
Embedded integrations are tested **inside a working copy of the host codebase** with the generated `plain_modules/` overlaid on top. Both `run_unittests_<lang>` and `run_conformance_tests_<lang>` follow this pattern — neither uses `PYTHONPATH` / `NODE_PATH` tricks to stitch two trees together at import time. The host *is* the runtime environment; the generated module is dropped into it and exercised as if it had always lived there.
|
|
82
|
+
|
|
83
|
+
This matters because the integration's generated code references host symbols by their full import path (e.g. `from host_project.integrations.base import IntegrationContract`). Those imports only resolve cleanly when the test process is rooted in the host's package layout — anything else creates path edge cases that bite later in conformance failures.
|
|
84
|
+
|
|
85
|
+
### The merge step (used by both `prepare_environment` and `run_unittests`)
|
|
86
|
+
|
|
87
|
+
Both scripts stage their own working copy under `.tmp/<lang>_<arg>/` per the shared testing-script rules (input folders are read-only). Inside that working folder:
|
|
88
|
+
|
|
89
|
+
1. **Copy the host codebase into the working folder.** Use a recursive copy (`rsync -a --delete`, `cp -R`, or `robocopy`) so each test run starts from a clean, identical host tree
|
|
90
|
+
2. **Overlay `plain_modules/<module>/` into the host's package tree at the target package path** recorded in the `host-codebase` concept (e.g. `plain_modules/integrations/<provider>/` → `<host_copy>/host_project/integrations/<provider>/`). Use a copy that overwrites — the generated module replaces any same-named files in the host copy
|
|
91
|
+
3. **Install dependencies inside the merged tree.** The host's own manifest (`pyproject.toml` / `package.json` / `go.mod` / …) drives the install. The integration's extra dependencies are layered on top by either (a) the renderer having written them into the host's manifest already, or (b) the script installing them explicitly after the host install
|
|
92
|
+
4. **Run the test command from inside the merged tree** — `cwd` is `<host_copy>`, and the test runner discovers tests using the host's normal layout
|
|
93
|
+
|
|
94
|
+
### Per-script responsibilities
|
|
95
|
+
|
|
96
|
+
- **`prepare_environment_<lang>`** performs the full merge once per render (host copy + plain_modules overlay + dependency install + any build artifacts) into `.tmp/<lang>_<arg>/`. The N subsequent `run_conformance_tests_<lang>` invocations attach to this populated folder (activate-only variant — see the shared testing-script rules)
|
|
97
|
+
- **`run_unittests_<lang>`** performs its **own** merge into its **own** `.tmp/<lang>_<arg>/` working folder — it does not share `prepare`'s folder, and it does not depend on `prepare` having run. The host copy + overlay + install steps are duplicated inside the unit-test script for self-containedness
|
|
98
|
+
- **`run_conformance_tests_<lang>`** does **not** re-merge. It `cd`s into the working folder that `prepare_environment` populated and runs the conformance command against the merged tree
|
|
99
|
+
|
|
100
|
+
### Language-specific merge primitives
|
|
101
|
+
|
|
102
|
+
| Language | Host copy | Overlay | Dependency install | Test invocation (inside merged tree) |
|
|
103
|
+
|----------|-----------|---------|--------------------|--------------------------------------|
|
|
104
|
+
| Python | `rsync -a <host>/ .tmp/python_<arg>/` | `rsync -a plain_modules/<module>/ .tmp/python_<arg>/<host_pkg_path>/` | `cd .tmp/python_<arg> && pip install -e .` (then integration extras) | `cd .tmp/python_<arg> && pytest …` |
|
|
105
|
+
| Node.js | `rsync -a <host>/ .tmp/node_<arg>/` | `rsync -a plain_modules/<module>/ .tmp/node_<arg>/<host_pkg_path>/` | `cd .tmp/node_<arg> && npm ci` (then integration extras) | `cd .tmp/node_<arg> && npm test …` |
|
|
106
|
+
| Go | `rsync -a <host>/ .tmp/go_<arg>/` | `rsync -a plain_modules/<module>/ .tmp/go_<arg>/<host_pkg_path>/` | `cd .tmp/go_<arg> && go mod tidy` | `cd .tmp/go_<arg> && go test ./…` |
|
|
107
|
+
| Java / Kotlin | `rsync -a <host>/ .tmp/java_<arg>/` | `rsync -a plain_modules/<module>/ .tmp/java_<arg>/<host_pkg_path>/` | `cd .tmp/java_<arg> && mvn -q -DskipTests install` | `cd .tmp/java_<arg> && mvn test …` |
|
|
108
|
+
| Rust | `rsync -a <host>/ .tmp/rust_<arg>/` | `rsync -a plain_modules/<module>/ .tmp/rust_<arg>/<host_pkg_path>/` | `cd .tmp/rust_<arg> && cargo fetch` | `cd .tmp/rust_<arg> && cargo test …` |
|
|
109
|
+
|
|
110
|
+
Adjust the language-specific install / test commands to whatever the host's manifest actually uses (Poetry instead of pip, pnpm instead of npm, Gradle instead of Maven, …). The merge primitive itself does not change.
|
|
111
|
+
|
|
112
|
+
### Invariants the scripts must enforce
|
|
113
|
+
|
|
114
|
+
- **Host root is a parameter, not a literal.** No script may hardcode an absolute host path. Read the host root from an env var (e.g. `HOST_CODEBASE_ROOT`) with a sensible default matching the user's layout (e.g. `../host_project`). Surface the env var in each script's `--help` / usage banner. Capture this env var in the integration's configuration concept so it has exactly one declared name across specs, scripts, and runtime
|
|
115
|
+
- **Target package path is read from the `host-codebase` concept** — never inferred from a heuristic. The renderer writes that path into the generated module's location too, so the overlay destination is unambiguous
|
|
116
|
+
- **The host source tree is read-only.** The merge writes into `.tmp/<lang>_<arg>/`; the user's `<host>` checkout is never modified. If a script appears to need to write into `<host>`, it is buggy — the working copy under `.tmp/` is what's mutable
|
|
117
|
+
- **Each merge is idempotent.** Re-running the script (or two scripts back-to-back) yields the same merged tree
|
|
118
|
+
- **No new `config.yaml` key is needed** — the merge happens inside the scripts. The renderer reads the `host-codebase` concept (for the package path) and the configuration concept (for `HOST_CODEBASE_ROOT`) to wire the script bodies correctly
|
|
119
|
+
- **`***test reqs***` must document the merge contract** — name the merge primitive (`rsync` / `cp -R` / `robocopy`), the env var the host root is read from, the target package path inside the host where `plain_modules/<module>/` is overlaid, and the language-appropriate install + test commands. The renderer reads this req and emits the right script bodies
|
|
120
|
+
|
|
121
|
+
## Embedded-specific completion checklist
|
|
122
|
+
|
|
123
|
+
Before declaring an embedded integration done, in addition to the shared checklist in [`integrations.md`](integrations.md):
|
|
124
|
+
|
|
125
|
+
- [ ] `host-codebase` concept records host root path, host language + version, host dependency manager + manifest, target package path, host base class import path, target generated-class FQN, and the host conventions the contract follows
|
|
126
|
+
- [ ] Contract spec carries renderer directives (target language, target file path, target class name, host base class to subclass, host-package pins) and **links** to the contract schema; no class body is inlined
|
|
127
|
+
- [ ] Every host symbol referenced in any spec uses its fully qualified import path and is tagged "imported from the host codebase; do not redefine"
|
|
128
|
+
- [ ] Every host file the integration touches has been added under `resources/host/` as a linked resource — no host file contents are inlined in any spec
|
|
129
|
+
- [ ] `forge-plain` Phase 2's tech-stack decisions are transcribed verbatim from the host (no independent stack choices)
|
|
130
|
+
- [ ] Host-package version pins are copied into `***implementation reqs***`
|
|
131
|
+
- [ ] `prepare_environment` copies the host into `.tmp/<lang>_<arg>/`, overlays `plain_modules/<module>/` at the target package path, installs the merged tree's dependencies, and is the working folder conformance attaches to
|
|
132
|
+
- [ ] `run_unittests` runs the same host-copy + overlay + install sequence into its **own** `.tmp/<lang>_<arg>/` and invokes the test runner from inside the merged tree
|
|
133
|
+
- [ ] `run_conformance_tests` `cd`s into `prepare_environment`'s populated working folder and runs the conformance command from there — it does not re-merge and does not use import-path stitching
|
|
134
|
+
- [ ] Host codebase root is read from a named env var (default value documented in each script's usage) — never hardcoded
|
|
135
|
+
- [ ] Target package path (where `plain_modules/<module>/` is overlaid inside the host copy) is read from the `host-codebase` concept — never inferred
|
|
136
|
+
- [ ] The host source tree itself is never written to — every script mutation lands in `.tmp/<lang>_<arg>/`
|
|
137
|
+
- [ ] A `***test reqs***` entry documents the merge contract (primitive used, env var name, target package path inside the host, install + test commands)
|
|
138
|
+
|
|
139
|
+
## Anti-patterns specific to embedded integrations
|
|
140
|
+
|
|
141
|
+
- **Choosing a different language, framework, or dependency manager than the host.** The host stack is inherited; cross-stack `requires` chains are forbidden by [`requires-modules.md`](requires-modules.md)
|
|
142
|
+
- **Redefining a host class under `plain_modules/`.** Reference the host symbol by FQN; let the renderer import it
|
|
143
|
+
- **Inlining a host base class body into the contract spec.** Add the host file as a linked resource under `resources/host/` and reference it
|
|
144
|
+
- **Hardcoding the host codebase path in any spec or script.** Read it from the env var declared in the configuration concept
|
|
145
|
+
- **Asking the user to design the integration's tech stack.** Read it from the host's manifest files
|
|
146
|
+
- **Authoring an integration spec that contradicts an existing integration in the same host** without first surfacing the conflict and getting explicit user confirmation
|
|
147
|
+
- **Wiring tests with `PYTHONPATH` / `NODE_PATH` / Go `replace` directives instead of physically merging `plain_modules/<module>/` into a host copy.** The import-stitching approach is forbidden — every embedded test run starts by overlaying the generated module onto a working copy of the host tree
|
|
148
|
+
- **Writing into the user's `<host>` checkout from any test script.** The host source is read-only; the merged tree lives in `.tmp/<lang>_<arg>/`
|
|
149
|
+
- **Sharing one `.tmp/` working folder between `run_unittests` and `run_conformance_tests`.** Each script stages its own copy; only `run_conformance_tests` attaches to the folder `prepare_environment` populated
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for authoring ***plain specs for REST API integrations deployed standalone
|
|
3
|
+
globs: "**/*.plain"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rules for **standalone** integration specs
|
|
7
|
+
|
|
8
|
+
When an integration `.plain` module is **standalone** — meaning the generated code in `plain_modules/` is the deployable artifact itself (a service, daemon, CLI, scheduled job, container, or library published to a registry) — these rules apply on top of the shared rules in [`integrations.md`](integrations.md). If anything below contradicts a guess made from memory, the rules here win.
|
|
9
|
+
|
|
10
|
+
Standalone means: the integration is a complete application in its own right, picks its own tech stack, owns its own lifecycle, and exposes its own contract surface to external consumers.
|
|
11
|
+
|
|
12
|
+
## The integration owns its tech stack
|
|
13
|
+
|
|
14
|
+
Unlike embedded integrations, a standalone integration has no host to inherit from. Every stack decision is **explicit**, captured during `forge-plain` Phase 2, and recorded in `***implementation reqs***`:
|
|
15
|
+
|
|
16
|
+
- **Implementation language** and its exact version
|
|
17
|
+
- **HTTP client library** (e.g. `httpx`, `requests`, `node-fetch`, `axios`, `go-resty`, `reqwest`) — pinned, with the auth/retry/timeout features the integration uses called out
|
|
18
|
+
- **Application framework** (Flask, FastAPI, Express, Fiber, Gin, Spring Boot, …) when the integration exposes HTTP; **CLI framework** (Click, argparse, `cobra`, `clap`) when it exposes a CLI
|
|
19
|
+
- **Data storage** (when needed): idempotency-key persistence, webhook deduplication store, OAuth token cache, response cache — pick the storage primitive (SQLite, Postgres, Redis, on-disk file, in-memory with LRU bounds) and pin it
|
|
20
|
+
- **Architecture and layering** — which patterns the codebase follows (clean architecture, hexagonal, simple flat package, …)
|
|
21
|
+
- **Packaging artifact** — published Python wheel / sdist, npm package, Go binary, container image, OS package, …; one or more, named explicitly
|
|
22
|
+
|
|
23
|
+
Standalone integrations may freely import any third-party library that isn't constrained by an existing host. Choose the smallest set that solves the problem.
|
|
24
|
+
|
|
25
|
+
## The contract schema is the public artifact
|
|
26
|
+
|
|
27
|
+
In an embedded integration, the schema feeds the renderer's code generation; the host codebase consumes the *generated class*. In a standalone integration, the schema is **shipped** to external consumers and the generated implementation is internal to `plain_modules/`.
|
|
28
|
+
|
|
29
|
+
- **Pick the publication surface and write it into the spec.** Examples (one functional spec per surface):
|
|
30
|
+
- HTTP service: "served at `/openapi.json` (and a Swagger UI at `/docs`)"
|
|
31
|
+
- npm-published library: "published as `<package>` to npm; `package.json` exports the schema as `./schema.json`"
|
|
32
|
+
- PyPI-published library: "published as `<package>` to PyPI; bundle ships `schema.json` as a package data file"
|
|
33
|
+
- CLI: "emitted as `--help` and as `<cli> schema` (JSON-on-stdout)"
|
|
34
|
+
- Queue worker: "published as a JSON Schema in the team's schema registry under `<topic>.v<version>.json`"
|
|
35
|
+
- Scheduled job: "published as JSON Schema in `resources/contract/job-config.schema.json` and linked from the runbook"
|
|
36
|
+
- **Schema versioning is part of the contract.** Pin the version in the schema's `$id` / OpenAPI `info.version` and follow semver. Backwards-incompatible changes ship under a new file path, never as mutations to the published version
|
|
37
|
+
- **The renderer ships the schema verbatim** — the spec must not restate fields from the schema, and the generated implementation must consume the same file as the published artifact (so the two cannot drift)
|
|
38
|
+
|
|
39
|
+
## All lifecycle stages must be explicit
|
|
40
|
+
|
|
41
|
+
A standalone integration has no host lifecycle to delegate to. Every stage that an embedded integration could skip is mandatory here, each one captured as its own functional spec:
|
|
42
|
+
|
|
43
|
+
- **Initialization** — one-time setup at startup. Read configuration, validate credentials proactively (eager) or defer to first call (lazy) — pick one and write the spec accordingly. Open connection pools, register webhook handlers, initialize observability
|
|
44
|
+
- **Credential refresh** — proactive (background timer / pre-expiry refresh) vs. reactive (refresh-on-401). Use a refresh lock to prevent thundering herd. Specify the lock primitive (in-process mutex, distributed lock in Redis, …) when the integration runs as more than one replica
|
|
45
|
+
- **Graceful shutdown** — drain in-flight requests with a deadline; what happens to retries still queued (abandoned, persisted, returned to caller as a specific error); what signals trigger shutdown (`SIGTERM`, container-stop hook, …)
|
|
46
|
+
- **Health checks** — define what "healthy" means for **this** integration: last successful provider call within a window, valid credentials, provider's own `/health` returning 2xx, dependent storage reachable. Spec the response shape (status code, body) for the health endpoint when one is exposed
|
|
47
|
+
- **Background loops** — cron jobs, polling workers, queue consumers: each gets its own functional spec covering trigger, interval, leader-election (if applicable), failure handling, and observability
|
|
48
|
+
|
|
49
|
+
## Entry-point types — pick deliberately, enumerate fully
|
|
50
|
+
|
|
51
|
+
A standalone integration exposes one or more **entry points** to its consumers. Mixing entry-point types (e.g. HTTP + queue worker + CLI) is allowed, but every entry point must be enumerated up front and gets at least one functional spec.
|
|
52
|
+
|
|
53
|
+
| Entry-point type | Public surface | Contract format |
|
|
54
|
+
|------------------|----------------|-----------------|
|
|
55
|
+
| HTTP service | One or more routes (path + method) | OpenAPI 3.1 (`resources/contract/<integration>.openapi.yaml`) |
|
|
56
|
+
| CLI | Command + subcommands + flags | JSON Schema for config + `--help` text seeded from the schema |
|
|
57
|
+
| Queue worker | Topic / partition + message envelope | JSON Schema per message type (`resources/contract/<topic>.schema.json`) |
|
|
58
|
+
| Scheduled job | Job name + cron expression + config schema | JSON Schema for config |
|
|
59
|
+
| Library | Public package surface (functions, classes) | JSON Schema for I/O + published-package manifest |
|
|
60
|
+
| Webhook receiver | Provider-driven inbound HTTP | JSON Schema per event type under `resources/webhooks/` (shared with embedded — see [`integrations.md`](integrations.md)) |
|
|
61
|
+
|
|
62
|
+
Each entry point gets its own functional spec describing what calling it does end-to-end (which Phase 1 specs it composes — auth, the endpoint call, the retry policy, the error handling).
|
|
63
|
+
|
|
64
|
+
## Side effects are first-class — spell them out
|
|
65
|
+
|
|
66
|
+
A standalone integration usually mutates state outside the provider call itself. Each side effect is captured as a functional spec describing:
|
|
67
|
+
|
|
68
|
+
- **What** gets mutated — local DB writes, file writes, cache updates, emitted domain events, metrics, log lines
|
|
69
|
+
- **When** the mutation happens — before or after the provider call
|
|
70
|
+
- **Is the mutation transactional with the provider call** — atomic, eventually consistent, fire-and-forget
|
|
71
|
+
- **What happens on partial failure** — provider call succeeded but the side effect failed (and vice versa); rollback / compensation / retry strategy
|
|
72
|
+
|
|
73
|
+
## Concurrency and backpressure
|
|
74
|
+
|
|
75
|
+
Standalone integrations face concurrent load that an embedded library doesn't. The spec must pin:
|
|
76
|
+
|
|
77
|
+
- **Sync vs. async** in the host-language sense (`async def`, `Promise`, goroutines, threads)
|
|
78
|
+
- **Expected peak throughput** (RPS) the consumer will issue
|
|
79
|
+
- **Connection pool size**
|
|
80
|
+
- **Request queue depth**
|
|
81
|
+
- **Backpressure behavior** when the rate limit (from Phase 1 topic 10 — see [`integrations.md`](integrations.md)) and the consumer's demand collide — drop, queue, block with timeout, or 429 the consumer
|
|
82
|
+
- **Concurrency limits on mutating operations** — when the provider has its own per-resource serialization constraints
|
|
83
|
+
|
|
84
|
+
Capture all of this as a concurrency concept and one functional spec for "apply backpressure when local demand exceeds rate-limit budget".
|
|
85
|
+
|
|
86
|
+
## Configuration surface
|
|
87
|
+
|
|
88
|
+
- Every config knob the consumer can set lives in `resources/config.schema.json` (JSON Schema): env var names, config file keys, secrets, feature flags, regional selectors, timeouts, retry counts, base URL overrides
|
|
89
|
+
- The configuration concept enumerates every key by name (type, default, required vs optional, validation, where it is read — startup vs per-call)
|
|
90
|
+
- Secrets are referenced by env var name only; never written into the schema as default values
|
|
91
|
+
- One functional spec covers "load and validate configuration on startup" — fail-fast on missing required values
|
|
92
|
+
|
|
93
|
+
## Versioning of the integration itself
|
|
94
|
+
|
|
95
|
+
Separate from the provider's API version (which lives in the provider OpenAPI file), the **integration's own contract** is versioned independently:
|
|
96
|
+
|
|
97
|
+
- Semver of the published library / OpenAPI `info.version` / schema `$id` URL / CLI `--version`
|
|
98
|
+
- Backwards-compatibility policy stated explicitly (e.g. "minor and patch are wire-compatible; breaking changes only on major")
|
|
99
|
+
- New major versions ship at a new schema path; never mutate a published version in place
|
|
100
|
+
|
|
101
|
+
Capture as a contract-version concept; pin the version in every published schema.
|
|
102
|
+
|
|
103
|
+
## Testing — live vs recorded, sandbox credentials, webhooks
|
|
104
|
+
|
|
105
|
+
Standalone integrations get tested in isolation, so the testing strategy must be explicit (capture each decision as a `***test reqs***` entry):
|
|
106
|
+
|
|
107
|
+
- **Live vs. recorded conformance tests.** Live tests hit the provider (requires sandbox creds in CI, may be rate-limited). Recorded tests use VCR-style cassettes or prerecorded responses under `resources/fixtures/`. Mock-server tests use a local stub (WireMock, Mockoon, MSW). Each has tradeoffs — pick one (or a mix) and document it
|
|
108
|
+
- **Sandbox credentials in CI.** If live tests are in scope, name where credentials come from (CI secret store, dedicated test tenant) and the rotation / leak-response policy
|
|
109
|
+
- **Webhook tests** (if webhooks are in scope) must cover signature verification end-to-end — including invalid signatures and replay attempts
|
|
110
|
+
- **Rate-limit tests.** Tests that exercise the 429 path must **not** exhaust the live API's quota — use a local mock for those cases
|
|
111
|
+
- **Idempotency tests.** Run the same mutating call twice (with the same idempotency key) and assert the same response — either against the live sandbox or against a recorded duplicate fixture
|
|
112
|
+
|
|
113
|
+
## Standalone-specific completion checklist
|
|
114
|
+
|
|
115
|
+
Before declaring a standalone integration done, in addition to the shared checklist in [`integrations.md`](integrations.md):
|
|
116
|
+
|
|
117
|
+
- [ ] Tech stack decisions are recorded in `***implementation reqs***` — language and version, HTTP client library and version, framework (HTTP / CLI / worker as applicable), data storage choice(s), architecture/layering
|
|
118
|
+
- [ ] Publication surface(s) for the contract schema are enumerated in functional specs (one per surface)
|
|
119
|
+
- [ ] Schema versioning strategy is captured — `$id` / `info.version` / package version pinned, backwards-compatibility policy stated
|
|
120
|
+
- [ ] Every entry point is enumerated and has at least one functional spec (HTTP routes, CLI commands, queue topics, scheduled jobs, library APIs)
|
|
121
|
+
- [ ] Every lifecycle stage has a functional spec — initialization, credential refresh, graceful shutdown, health check
|
|
122
|
+
- [ ] Side effects are each captured as a functional spec with ordering, transactionality, and partial-failure handling
|
|
123
|
+
- [ ] Concurrency model is captured (sync vs async, pool size, queue depth, backpressure strategy)
|
|
124
|
+
- [ ] Configuration surface lives in `resources/config.schema.json` and is linked from the configuration concept
|
|
125
|
+
- [ ] Testing strategy is recorded in `***test reqs***` — live vs recorded, sandbox credential source, webhook signature coverage, rate-limit-test isolation
|
|
126
|
+
|
|
127
|
+
## Anti-patterns specific to standalone integrations
|
|
128
|
+
|
|
129
|
+
- **Skipping any lifecycle stage** because it "feels small". Standalone integrations have no host to fall back on; an implicit shutdown is an undefined shutdown
|
|
130
|
+
- **Inlining fields from `resources/contract/<entry-point>.schema.json` into a functional spec.** The schema is the source of truth and is **published** — duplicating fields in spec text guarantees drift
|
|
131
|
+
- **Mutating a published schema's `$id` / version path** instead of shipping a new file. Consumers may already be pinning the old version
|
|
132
|
+
- **Treating configuration as "whatever env vars seem useful at runtime".** Every key lives in `resources/config.schema.json` with type, default, and validation; the loader fails fast on missing required values
|
|
133
|
+
- **Picking a tech stack incrementally during Phase 3.** Decide it once at the top of `forge-plain` Phase 2 and transcribe into `***implementation reqs***`; don't accrete a stack across several specs
|
|
134
|
+
- **Hitting the live provider in rate-limit (429) or destructive-error (500) tests.** Use a local mock; live calls are for the happy path and for sandbox-safe error paths
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for authoring ***plain specs for REST API integrations
|
|
3
|
+
globs: "**/*.plain"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rules for writing integration specs in `***plain`
|
|
7
|
+
|
|
8
|
+
When writing or editing a `.plain` file that describes an **integration** against a REST API (synchronous JSON/HTTP request-response plus webhook callbacks), always follow these rules. Non-REST integrations (gRPC, GraphQL, SOAP, message brokers, raw TCP, file drops) are out of scope of these rules.
|
|
9
|
+
|
|
10
|
+
## Scope of "integration" specs
|
|
11
|
+
- An integration `.plain` module describes how the project talks to a **third-party or internal REST API**
|
|
12
|
+
- The integration may be **embedded** (lives as a library/module inside an existing host codebase) — see [`integration-embedded.md`](integration-embedded.md) for the additional rules that apply
|
|
13
|
+
- Or **standalone** (a service, daemon, CLI, scheduled job, or container) — see [`integration-standalone.md`](integration-standalone.md) for the additional rules that apply
|
|
14
|
+
- The contract surface, edge-case coverage, live-API cross-check, and `resources/` layout described below apply to **both** shapes — the shape-specific rule files add to them, never replace them
|
|
15
|
+
|
|
16
|
+
## Contract artifacts live in `resources/` (hard rule)
|
|
17
|
+
|
|
18
|
+
**Every structural contract the integration deals with — endpoint definitions, request/response schemas, error envelopes, webhook payloads, pagination envelopes, rate-limit headers, the integration's own I/O contract — lives in `resources/` as a linked resource.** Concepts and functional specs **reference** these files; they never restate fields, types, status codes, or header names inline.
|
|
19
|
+
|
|
20
|
+
Pick the right format per artifact:
|
|
21
|
+
|
|
22
|
+
| Artifact | Format | Conventional path |
|
|
23
|
+
|----------|--------|-------------------|
|
|
24
|
+
| Provider's REST surface | OpenAPI 3.1 (YAML or JSON) | `resources/<provider>.openapi.yaml` |
|
|
25
|
+
| Webhook payload (per event type) | JSON Schema Draft 2020-12 | `resources/webhooks/<event>.schema.json` |
|
|
26
|
+
| Rich webhook contracts (multi-channel) | AsyncAPI 2.6+ | `resources/webhooks.asyncapi.yaml` |
|
|
27
|
+
| Integration's own I/O contract | JSON Schema or OpenAPI | `resources/contract/<entry-point>.schema.json` |
|
|
28
|
+
| Configuration surface | JSON Schema | `resources/config.schema.json` |
|
|
29
|
+
| Error code → category mapping | YAML enum | `resources/error-map.yaml` |
|
|
30
|
+
| Rate-limit header inventory | YAML enum | `resources/rate-limit-headers.yaml` |
|
|
31
|
+
| Retry policy parameters | YAML enum | `resources/retry-policy.yaml` |
|
|
32
|
+
| Captured probe responses | Raw JSON | `resources/fixtures/<endpoint>.<case>.json` |
|
|
33
|
+
|
|
34
|
+
Rules that flow from this:
|
|
35
|
+
|
|
36
|
+
- **Concepts carry references, not data.** An endpoint concept names the endpoint and points at its OpenAPI `paths.<path>.<method>` entry; it does not duplicate the request/response shape in concept attributes. Same for webhook concepts, error-model concepts, pagination concepts, etc.
|
|
37
|
+
- **Functional specs consume linked resources.** A spec describes **behavior** ("call endpoint X, parse the response, classify errors, retry on 5xx") and links to the resource that supplies the **shape**. Field names, types, and validation rules live in the resource file.
|
|
38
|
+
- **Schemas are versioned by file path.** When the provider releases a new API version, copy the new OpenAPI file to a new path (e.g. `resources/<provider>.v2.openapi.yaml`); never mutate the v1 file in place.
|
|
39
|
+
- **The renderer generates language-native types from the resources.** For embedded integrations the renderer reads JSON Schema / OpenAPI components and emits Pydantic / TypeScript / Go types in `plain_modules/`. The spec declares **which** schema to generate from, **where** the generated type should land, and **what host base class** it must subclass — but never restates the schema's fields.
|
|
40
|
+
|
|
41
|
+
A single `.plain` module can (and typically will) reference many resources. That is the intended pattern; do not try to collapse them into one mega-file.
|
|
42
|
+
|
|
43
|
+
## Live API must be cross-checked against the documentation
|
|
44
|
+
|
|
45
|
+
Documentation lies — it goes stale, omits undocumented fields, describes a different API version, papers over breaking changes. Every integration spec must be grounded in what the API really returns, not what the docs claim it returns.
|
|
46
|
+
|
|
47
|
+
- **Validate credentials against the live API** before authoring downstream specs. A 2xx on a low-risk read-only endpoint (`/v1/me`, `/account`, `/whoami`, `/health`) is the gate. On 401/403, stop and resolve before continuing.
|
|
48
|
+
- **Issue the minimum cross-check coverage** with `fetch`: one discovery / schema endpoint if available, one list endpoint per primary entity in scope, one single-object retrieval per primary entity, one empty/boundary response, one 404, one 400/422, and one deliberate 401.
|
|
49
|
+
- **Save every probe response under `resources/fixtures/`** with credentials redacted. The fixtures become the seed for `resources/<provider>.openapi.yaml` and feed conformance tests later.
|
|
50
|
+
- **Every discrepancy is recorded, not smoothed over.** Each finding goes into the relevant resource (the OpenAPI file, the error envelope schema, `rate-limit-headers.yaml`, …) as the source of truth, with a short note in the corresponding concept saying "docs claim X, live API returns Y; we follow the live API".
|
|
51
|
+
- **Only `GET` / `HEAD` / `OPTIONS` on the cross-check.** Mutating calls (`POST`, `PATCH`, `PUT`, `DELETE`) require explicit per-call user confirmation and must target a sandbox account.
|
|
52
|
+
- **Credentials are never written to `.plain` files or summaries.** Reference them by env-var name only.
|
|
53
|
+
|
|
54
|
+
## Embedded vs standalone — pick the shape early
|
|
55
|
+
|
|
56
|
+
Every integration is either **embedded** (lives as a library/module inside an existing host codebase) or **standalone** (a service, daemon, CLI, scheduled job, or container). The choice is captured as a concept (`integration-shape: embedded | standalone`) so later specs can reference it.
|
|
57
|
+
|
|
58
|
+
The contract artifact itself is **identical** across both shapes: a JSON Schema (or OpenAPI) file under `resources/contract/`. What changes is **what the renderer emits from it** and **what extra context the spec carries**:
|
|
59
|
+
|
|
60
|
+
- **Embedded** → the renderer generates a host-language class from the schema and wires it into the host's import path. The host codebase dictates the tech stack — see [`integration-embedded.md`](integration-embedded.md) for the full ruleset
|
|
61
|
+
- **Standalone** → the renderer treats the schema as the public artifact: it generates an internal implementation in `plain_modules/` and ships the schema verbatim for external consumers. The integration owns its stack — see [`integration-standalone.md`](integration-standalone.md) for the full ruleset
|
|
62
|
+
|
|
63
|
+
## Edge-case coverage is a hard floor, not a stretch goal
|
|
64
|
+
|
|
65
|
+
A production-ready integration spec captures every corner case the API can throw at the integration. Each of the following must be in the specs (or explicitly recorded as "not applicable" / "not in scope" with the user's acknowledgement) before the integration is considered complete:
|
|
66
|
+
|
|
67
|
+
- **Provider, purpose, canonical documentation URL(s)** as concepts
|
|
68
|
+
- **Endpoints in scope** in `resources/<provider>.openapi.yaml`, one `paths.<path>.<method>` entry per endpoint; endpoint concepts link to the OpenAPI entry
|
|
69
|
+
- **Auth scheme**, credential source pinned by env-var name, refresh policy, scopes — with `components.securitySchemes` in the OpenAPI matching
|
|
70
|
+
- **Environments** (sandbox / staging / production) and the switch mechanism — reflected in `servers` of the OpenAPI file
|
|
71
|
+
- **API version pinning** strategy (URL path, header, `Accept`, query string) and the deprecation policy
|
|
72
|
+
- **Request serialization** (content type, date format, numeric precision, custom headers) in `components.schemas` / `components.parameters`
|
|
73
|
+
- **Pagination model** as `components.schemas.PageEnvelope` plus `resources/pagination.yaml` (style, defaults, safety cap)
|
|
74
|
+
- **Rate-limit model** as `resources/rate-limit-headers.yaml` + `resources/rate-limits.yaml` + `components.schemas.RateLimitError`
|
|
75
|
+
- **Error model** as `components.schemas.ErrorEnvelope` + `resources/error-map.yaml`, with one functional spec per error category
|
|
76
|
+
- **Retry policy** in `resources/retry-policy.yaml`
|
|
77
|
+
- **Idempotency strategy** in `resources/idempotency.yaml` + idempotency header in `components.parameters`
|
|
78
|
+
- **Webhook contracts** as `resources/webhooks/<event>.schema.json` per event type + `resources/webhook-signing.yaml`
|
|
79
|
+
- **Data mapping** (entity schemas + transformations / exclusions) as entity schemas + `resources/data-mapping.yaml`
|
|
80
|
+
- **Compliance / data-sensitivity** constraints (PII, PHI, payment data, data residency, log redaction, audit logs)
|
|
81
|
+
- **Observability** (log fields, provider request IDs, metrics, tracing propagation)
|
|
82
|
+
|
|
83
|
+
## Anti-patterns (do not do these)
|
|
84
|
+
|
|
85
|
+
- **Restating an OpenAPI / JSON Schema field list in a concept or functional spec.** The schema lives in `resources/`; the spec links to it
|
|
86
|
+
- **Pasting a webhook payload, error envelope, or list-endpoint response inline.** Save it as a fixture under `resources/fixtures/` and link the fixture
|
|
87
|
+
- **Inlining a host base class body into the contract spec.** Add the host file as a linked resource under `resources/host/` and reference it by FQN
|
|
88
|
+
- **Embedding credentials, tokens, or signing keys in a `.plain` file or in a summary** — credentials are referenced by env-var name only
|
|
89
|
+
- **Authoring against unverified credentials.** Validate first; if the user has no credentials yet, flag it in the module's frontmatter description and re-validate once credentials arrive
|
|
90
|
+
- **`requires`-ing a separate-stack module** (a Python backend `requires`-ing a React frontend, or vice versa) — see [`requires-modules.md`](requires-modules.md). Use a shared API schema in `resources/` instead
|
|
91
|
+
- **Authoring Phase 1 specs from the docs first and "reconciling" with the live API later.** Probe the API as you reach each topic; the live response is the source of truth from the moment it's captured
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Line-length and bullet-continuation rules for every section in .plain files
|
|
3
|
+
globs: "**/*.plain"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rules for line length in `.plain` files
|
|
7
|
+
|
|
8
|
+
These rules apply to **every** section — `***definitions***`, `***implementation reqs***`, `***test reqs***`, `***functional specs***`, `***acceptance tests***` — and to concept explanations alike.
|
|
9
|
+
|
|
10
|
+
## Hard limit: 120 characters per line
|
|
11
|
+
- If a line exceeds 120 characters, split it at a natural clause boundary into nested `- ` bullets
|
|
12
|
+
- Concision is in service of clarity, never the other way around — wordy-but-precise always beats terse-but-ambiguous
|
|
13
|
+
- Prefer short, direct sentences and plain words; if a 10-cent word and a 50-cent word say the same thing, use the 10-cent one
|
|
14
|
+
|
|
15
|
+
## Never use bare continuation lines (invalid ***plain syntax)
|
|
16
|
+
- ***plain syntax requires every line inside a section to be its own list item starting with `- `
|
|
17
|
+
- Indented continuation lines without a leading `- ` are syntactically invalid — they look reasonable to a human reader but the renderer cannot parse them
|
|
18
|
+
- When a sentence is too long, **break it into multiple bullet items**, each on its own line, nested under the parent bullet so the meaning stays grouped
|
|
19
|
+
|
|
20
|
+
## Examples
|
|
21
|
+
|
|
22
|
+
BAD — line is too long:
|
|
23
|
+
|
|
24
|
+
```plain
|
|
25
|
+
- :GatewayWebhook: should hand off :StripeRequest: to :StripeIntegration:.handle(), which returns a list of :EventEnvelope: dicts conforming to the gateway's contract.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
WRONG SYNTAX (AVOID AT ALL COSTS) — bare indented continuation without a leading `- `:
|
|
29
|
+
|
|
30
|
+
```plain
|
|
31
|
+
- :GatewayWebhook: should hand off :StripeRequest: to :StripeIntegration:.handle(),
|
|
32
|
+
which returns a list of :EventEnvelope: dicts conforming to the gateway's
|
|
33
|
+
contract.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
GOOD — split at a natural clause boundary into nested `- ` bullets:
|
|
37
|
+
|
|
38
|
+
```plain
|
|
39
|
+
- :GatewayWebhook: should hand off :StripeRequest: to :StripeIntegration:.handle()
|
|
40
|
+
- The method returns a list of :EventEnvelope: dicts.
|
|
41
|
+
- The dicts must conform to the gateway's :EventEnvelope: contract.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What never goes inline
|
|
45
|
+
- Long URLs, schema fragments, or example payloads — those belong in `resources/` per [`linked-resources.md`](linked-resources.md)
|
|
46
|
+
- If you find yourself pasting a multi-line block into a spec line, stop and link the file instead
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for linking external resources from .plain specs
|
|
3
|
+
globs: "**/*.plain"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rules for linked resources in `.plain` files
|
|
7
|
+
|
|
8
|
+
Specifications can reference external files using markdown link syntax. The renderer reads the linked file's bytes verbatim and feeds them to the model alongside the spec. That mechanism only works for a specific shape of target — violating any of the rules below is one of the most common and disruptive mistakes in `.plain` authoring.
|
|
9
|
+
|
|
10
|
+
## Hard constraint: a linked resource is always a single, text-based file on disk
|
|
11
|
+
|
|
12
|
+
A linked resource **must not** be any of the following:
|
|
13
|
+
|
|
14
|
+
1. **A folder / directory.** `[integrations](src/integrations/)`, `[host project](../host_project/)` are invalid — the renderer cannot ingest a directory. Pick the single most representative file inside (a `README.md`, an exemplar source file, a manifest) and link **that**.
|
|
15
|
+
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, fetch it once, save the response to a text file under `resources/`, and link the saved file.
|
|
16
|
+
3. **A binary file.** PNG, JPG, GIF, PDF, DOCX, XLSX, ZIP, MP3, MP4, compiled binaries (`.exe`, `.so`, `.class`, `.wasm`), and anything else that isn't human-readable text in its raw form. Binary content cannot be meaningfully consumed by the renderer — transcribe it into a text-based form first (a UI screenshot becomes a Markdown description, a PDF spec becomes a Markdown extract or the underlying JSON Schema / OpenAPI, an architecture diagram becomes a Mermaid block).
|
|
17
|
+
|
|
18
|
+
If the markdown-link target ends with `/`, contains `://`, resolves to a directory, or has a binary extension, **stop** — it cannot be a linked resource. Convert it first.
|
|
19
|
+
|
|
20
|
+
## URLs and folder paths must not appear *anywhere* in `.plain` content
|
|
21
|
+
- The constraint is not only about markdown links — URLs and folder paths must not appear **anywhere** in `.plain` content (concept body prose, functional-spec text, implementation reqs, test reqs)
|
|
22
|
+
- The renderer cannot follow URLs or open folders; a URL in prose is a *ghost dependency* — it looks meaningful to a human reader but contributes nothing to code generation, and the spec silently drifts from reality
|
|
23
|
+
- **The only exception** is for URLs and paths that are *values the produced software itself uses at runtime* — the base URL an integration calls, a database connection path, a CLI argument default. Those are configuration values, not external references
|
|
24
|
+
- Litmus test: "Would the renderer benefit from reading the bytes at this URL / folder?" If yes, save it to a file and link the file. If no (it's a runtime value the code carries forward), it can stay as plain text
|
|
25
|
+
|
|
26
|
+
## Structured protocol artifacts must be linked, never transcribed
|
|
27
|
+
- JSON Schema, OpenAPI / Swagger, GraphQL SDL, Protobuf `.proto`, Avro / Thrift schemas, XML XSDs, AsyncAPI specs, JSON-RPC method definitions, wire-protocol descriptions, payload examples — anything with a formal machine-readable shape — belongs in a file under `resources/` (or a subfolder of the `.plain` file's directory)
|
|
28
|
+
- The 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
|
|
29
|
+
- Reasons: one source of truth (no drift between prose and schema); the renderer and the generated code can both consume the file directly; schema changes show up cleanly as diffs
|
|
30
|
+
|
|
31
|
+
```plain
|
|
32
|
+
***definitions***
|
|
33
|
+
|
|
34
|
+
- :TaskCreateRequest: is the JSON payload for creating a task, defined by
|
|
35
|
+
[resources/task_create_request.schema.json](resources/task_create_request.schema.json).
|
|
36
|
+
- :TasksAPI: is the public HTTP surface for tasks, defined by
|
|
37
|
+
[resources/tasks_openapi.yaml](resources/tasks_openapi.yaml).
|
|
38
|
+
|
|
39
|
+
***functional specs***
|
|
40
|
+
|
|
41
|
+
- :User: should be able to add :Task: by POSTing :TaskCreateRequest: to the
|
|
42
|
+
`POST /tasks` endpoint of :TasksAPI:. The endpoint responds per :TasksAPI:.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Each linked resource is referenced from exactly one place
|
|
46
|
+
- Linking the same file from two functional specs (or from a functional spec **and** an implementation requirement) creates two independent sources of truth — any later edit silently diverges
|
|
47
|
+
- If a resource needs to inform multiple parts of the project, **don't repeat the link** — attach the resource to a **concept** in `***definitions***` and reference the concept token (`:ConceptName:`) elsewhere
|
|
48
|
+
- If you're about to paste the same `[name](path)` link a second time, **stop** — create the concept first
|
|
49
|
+
|
|
50
|
+
```plain
|
|
51
|
+
***definitions***
|
|
52
|
+
|
|
53
|
+
- :TaskModalSpec: is the user-interface contract for the task modal,
|
|
54
|
+
fully described in [task_modal_specification.yaml](task_modal_specification.yaml).
|
|
55
|
+
|
|
56
|
+
***functional specs***
|
|
57
|
+
|
|
58
|
+
- :User: should be able to add :Task: using :TaskModalSpec:.
|
|
59
|
+
|
|
60
|
+
- :User: should be able to edit :Task: using :TaskModalSpec:.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## File location and path resolution
|
|
64
|
+
- Paths are resolved relative to the `.plain` file's directory
|
|
65
|
+
- Only files in the same folder (and subfolders) can be linked
|
|
66
|
+
- The conventional location is `resources/` under the `.plain` file's directory
|
|
@@ -10,8 +10,14 @@ When creating or editing a `.plain` file that uses `requires`, always follow the
|
|
|
10
10
|
## What requires does
|
|
11
11
|
- `requires` establishes a **build ordering** — the required module is built before the current one
|
|
12
12
|
- The required module's generated code (`plain_modules/<required_module>`) is copied as the starting point
|
|
13
|
-
- The required module's `***functional specs***` become visible as **previous functional specs**
|
|
14
|
-
- Only `exported_concepts` from the required module are available — not its full definitions
|
|
13
|
+
- The required module's `***functional specs***` become visible as **previous functional specs** — this property **is transitive**
|
|
14
|
+
- Only `exported_concepts` from the required module are available — not its full definitions — and this property **is not transitive**
|
|
15
|
+
|
|
16
|
+
## Tech stack must match (hard rule)
|
|
17
|
+
- Because the required module's generated code is copied as the starting point and the renderer continues building on top of it with a single toolchain, two modules can only be linked with `requires` when they target the **same language, framework, and runtime**
|
|
18
|
+
- A runtime / network dependency between systems is **not** a reason to use `requires`
|
|
19
|
+
- Example of the mistake: a React frontend that talks to a Python/FastAPI backend over HTTP must **not** `requires: [backend]` — the stacks differ
|
|
20
|
+
- Model that pair as two independent root modules (each with its own `config.yaml` and test scripts) and express the contract through a shared API schema in `resources/` or shared concepts in an `import`ed template — never through `requires`
|
|
15
21
|
|
|
16
22
|
## Build order, not necessarily dependency
|
|
17
23
|
- The current module does not need to extend or depend on the required module's code
|