plain-forge 1.0.18 → 1.0.20
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/README.md +17 -10
- package/bin/cli.mjs +53 -24
- package/forge/rules/bullet-continuation.md +59 -0
- package/forge/rules/definitions.md +15 -0
- package/forge/rules/func-specs.md +52 -7
- package/forge/rules/impl-reqs.md +1 -1
- package/forge/rules/integration-embedded-testing.md +7 -4
- package/forge/rules/integration-embedded.md +4 -1
- package/forge/rules/linked-resources.md +7 -6
- package/forge/rules/requires-modules.md +1 -1
- package/forge/rules/test-reqs.md +1 -1
- package/forge/skills/add-acceptance-test/SKILL.md +3 -25
- package/forge/skills/add-concept/SKILL.md +3 -22
- package/forge/skills/add-feature/SKILL.md +71 -67
- package/forge/skills/add-feature/references/checklist.md +38 -0
- package/forge/skills/add-feature/references/phase-0-intent.md +29 -0
- package/forge/skills/add-functional-spec/SKILL.md +3 -23
- package/forge/skills/add-functional-specs/SKILL.md +3 -24
- package/forge/skills/add-implementation-requirement/SKILL.md +4 -23
- package/forge/skills/add-resource/SKILL.md +7 -8
- package/forge/skills/add-test-requirement/SKILL.md +4 -21
- package/forge/skills/analyze-2-func-specs/SKILL.md +10 -10
- package/forge/skills/analyze-func-specs/SKILL.md +10 -10
- package/forge/skills/analyze-if-func-spec-too-complex/SKILL.md +93 -31
- package/forge/skills/break-down-func-spec/SKILL.md +78 -29
- package/forge/skills/create-import-module/SKILL.md +7 -0
- package/forge/skills/create-requires-module/SKILL.md +3 -2
- package/forge/skills/debug-specs/SKILL.md +5 -5
- package/forge/skills/forge-plain/SKILL.md +80 -299
- package/forge/skills/forge-plain/references/checklist.md +66 -0
- package/forge/skills/forge-plain/references/phase-0-intent.md +29 -0
- package/forge/skills/forge-plain/references/phase-1-product.md +40 -0
- package/forge/skills/forge-plain/references/phase-2-tech.md +24 -0
- package/forge/skills/forge-plain/references/phase-3-testing.md +66 -0
- package/forge/skills/forge-plain/references/phase-4-validate-handoff.md +49 -0
- package/forge/skills/implement-conformance-testing-script/SKILL.md +2 -2
- package/forge/skills/implement-prepare-environment-script/SKILL.md +2 -2
- package/forge/skills/implement-unit-testing-script/SKILL.md +1 -1
- package/forge/skills/init-config-file/SKILL.md +4 -3
- package/forge/skills/init-plain-project/SKILL.md +4 -0
- package/forge/skills/init-plain-project/references/checklist.md +31 -0
- package/forge/skills/load-codeplain-reference/SKILL.md +188 -0
- package/forge/skills/load-plain-reference/SKILL.md +48 -678
- package/forge/skills/load-plain-reference/references/project-model.md +48 -0
- package/forge/skills/load-plain-reference/references/rendering-and-testing.md +57 -0
- package/forge/skills/plain-healthcheck/SKILL.md +3 -3
- package/forge/skills/resolve-spec-conflict/SKILL.md +13 -5
- package/forge/skills/run-codeplain/SKILL.md +15 -15
- package/package.json +2 -2
- package/forge/rules/line-length.md +0 -45
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# plain-forge
|
|
6
6
|
|
|
7
|
-
A toolkit for working with [
|
|
7
|
+
A toolkit for working with [∗∗∗plain](https://plainlang.org) projects from inside your AI coding agent of choice — Claude Code, Codex, ForgeCode, OpenCode, and any other agent that reads from a standard skills directory. plain-forge ships skills, rules, and docs that turn a conversation into complete `.plain` spec files, then keeps maintaining them across the lifetime of the project. The specs are rendered into production-ready code by the [codeplain](https://codeplain.ai) renderer.
|
|
8
8
|
|
|
9
9
|
## What plain-forge does
|
|
10
10
|
|
|
@@ -20,7 +20,7 @@ Pick whichever entry point matches how much upfront design you want:
|
|
|
20
20
|
### 2. Grow an existing project
|
|
21
21
|
|
|
22
22
|
- **`add-feature`** — takes a feature request in plain English and runs the same one-question-at-a-time loop that `forge-plain` uses, scoped to a single feature against an existing `.plain` file.
|
|
23
|
-
- **Per-section authoring skills** — `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-requirement`, `add-concept`, `add-acceptance-test`, `add-resource`, `add-template`. Each enforces the relevant ***plain syntax rules (concept uniqueness, complexity limits,
|
|
23
|
+
- **Per-section authoring skills** — `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-requirement`, `add-concept`, `add-acceptance-test`, `add-resource`, `add-template`. Each enforces the relevant ***plain syntax rules (concept uniqueness, complexity limits, bullet syntax, linked-resource constraints, …) so hand-authoring doesn't drift from the language.
|
|
24
24
|
- **Module-management skills** — `create-import-module`, `create-requires-module`, `refactor-module`, `consolidate-concepts` for restructuring as the project grows.
|
|
25
25
|
|
|
26
26
|
### 3. Validate and maintain
|
|
@@ -62,11 +62,15 @@ npx plain-forge install --agent claude --scope project
|
|
|
62
62
|
|-----------|---------------|----------|
|
|
63
63
|
| `claude` | `.claude/` · `~/.claude/` | You use Claude Code |
|
|
64
64
|
| `codex` | `.agents/` · `~/.agents/` | You use the OpenAI Codex CLI |
|
|
65
|
+
| `copilot` | `.agents/` · `~/.agents/` | You use GitHub Copilot |
|
|
65
66
|
| `forgecode` | `.forge/` · `~/forge/` | You use ForgeCode |
|
|
66
67
|
| `opencode` | `.opencode/` · `~/.config/opencode/` | You use OpenCode |
|
|
67
68
|
| `universal` | `.agents/` · `~/.agents/` | You want a runtime-neutral layout that any agent reading from `.agents/` can pick up |
|
|
68
69
|
|
|
69
|
-
> **Why
|
|
70
|
+
> **Why several agents share `.agents/`.** Codex and GitHub Copilot both discover skills from
|
|
71
|
+
> `.agents/skills` (`~/.agents/skills` globally), which is also the runtime-neutral `universal`
|
|
72
|
+
> layout. ForgeCode instead reads `.forge/skills` (`~/forge/skills` globally). The manifest records
|
|
73
|
+
> which agent label you chose so `update` and `uninstall` report it correctly.
|
|
70
74
|
|
|
71
75
|
**Scope options:**
|
|
72
76
|
|
|
@@ -88,16 +92,19 @@ Each install writes three subfolders under the chosen directory:
|
|
|
88
92
|
|
|
89
93
|
#### How the rules get applied per agent
|
|
90
94
|
|
|
91
|
-
The `skills/` are discovered natively by every supported agent. The `
|
|
95
|
+
The `skills/` are discovered natively by every supported agent. The `load-plain-reference` skill
|
|
96
|
+
loads only the rules relevant to the current task and skips rules already present in context. Some
|
|
97
|
+
agents additionally support native rules wiring:
|
|
92
98
|
|
|
93
99
|
| Agent | How rules are applied |
|
|
94
100
|
|-------|-----------------------|
|
|
95
101
|
| **Claude Code** | Native — Claude Code auto-loads `.claude/rules/*.md` at launch. Each rule carries `paths: "**/*.plain"` frontmatter so Claude scopes it to `.plain` files. No extra wiring. |
|
|
96
102
|
| **OpenCode** | OpenCode doesn't auto-load a `rules/` dir; it reads globs from the `instructions` array in `opencode.json`. plain-forge **merges** a rules glob there — `.opencode/rules/*.md` (project, written to `./opencode.json`) or the absolute `~/.config/opencode/rules/*.md` (global, written to `~/.config/opencode/opencode.json`; absolute because OpenCode doesn't expand `~`). |
|
|
97
|
-
| **
|
|
98
|
-
| **Universal** |
|
|
103
|
+
| **ForgeCode** | plain-forge also appends a fenced managed pointer block to `AGENTS.md`: repo-root `./AGENTS.md` for project installs or `~/forge/AGENTS.md` globally. |
|
|
104
|
+
| **Codex / GitHub Copilot / Universal** | No extra instruction file is generated. Their discovered `load-plain-reference` skill loads applicable files from `.agents/rules/` when needed. |
|
|
99
105
|
|
|
100
|
-
|
|
106
|
+
When plain-forge touches `opencode.json` or ForgeCode's `AGENTS.md`, it merges into existing content
|
|
107
|
+
and leaves unrelated configuration intact. `uninstall` removes only plain-forge's glob or block.
|
|
101
108
|
|
|
102
109
|
`install` refuses to run if plain-forge is already present at the target directory — it prints a message pointing you at `update` and exits non-zero. Use `update` (below) to refresh an existing install.
|
|
103
110
|
|
|
@@ -129,7 +136,7 @@ npx plain-forge uninstall --agent claude --scope global
|
|
|
129
136
|
|
|
130
137
|
| Flag | Default | Values |
|
|
131
138
|
|------|---------|--------|
|
|
132
|
-
| `--agent` | `*` (all agents) | `claude`, `codex`, `forgecode`, `opencode`, `universal`, or `*` |
|
|
139
|
+
| `--agent` | `*` (all agents) | `claude`, `codex`, `copilot`, `forgecode`, `opencode`, `universal`, or `*` |
|
|
133
140
|
| `--scope` | `project` | `project` (cwd) or `global` (home directory) |
|
|
134
141
|
|
|
135
142
|
If an install has **no manifest** (e.g. one that predates manifests), `uninstall` cannot tell which files are plain-forge's, so it refuses to delete anything: it prints an error, lists the directories to clean up by hand, and exits non-zero. Refresh such an install with `update` first (which writes a manifest going forward), then `uninstall`.
|
|
@@ -188,7 +195,7 @@ Hit a bug in the rendered app, a failing test, or behavior that doesn't match wh
|
|
|
188
195
|
2. plain-forge applies the fix in the `.plain` file(s) only and summarizes what changed.
|
|
189
196
|
3. Re-render to regenerate the code (see [Rendering specs](#rendering-specs)).
|
|
190
197
|
|
|
191
|
-
> **Important:** Never edit generated
|
|
198
|
+
> **Important:** Never edit generated output under `plain_modules/` directly — neither the code in `plain_modules/<module>/code/` nor the conformance tests in `plain_modules/<module>/tests/`. Your changes will be overwritten on the next render. Always fix the spec and re-render.
|
|
192
199
|
|
|
193
200
|
|
|
194
201
|
## Repository Structure
|
|
@@ -209,7 +216,7 @@ test/
|
|
|
209
216
|
package.json # ships only `bin/cli.mjs` and `forge/` to npm
|
|
210
217
|
```
|
|
211
218
|
|
|
212
|
-
On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them into the chosen agent directory (`.claude/`, `.agents/` for Codex/universal, `.forge/`, or `.opencode/` — see the per-agent table above for global-scope paths), recording every file it wrote in `<agent-dir>/.plain-forge/manifest.json` so `update` can later refresh and prune precisely. For agents that need it, it also wires the rules into `opencode.json` or `AGENTS.md` (see [How the rules get applied per agent](#how-the-rules-get-applied-per-agent)).
|
|
219
|
+
On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them into the chosen agent directory (`.claude/`, `.agents/` for Codex/Copilot/universal, `.forge/`, or `.opencode/` — see the per-agent table above for global-scope paths), recording every file it wrote in `<agent-dir>/.plain-forge/manifest.json` so `update` can later refresh and prune precisely. For agents that need it, it also wires the rules into `opencode.json` or ForgeCode's `AGENTS.md` (see [How the rules get applied per agent](#how-the-rules-get-applied-per-agent)).
|
|
213
220
|
|
|
214
221
|
## Available Skills
|
|
215
222
|
|
package/bin/cli.mjs
CHANGED
|
@@ -13,7 +13,8 @@ const forgeDir = path.join(pkgRoot, "forge");
|
|
|
13
13
|
// These are the directories each tool actually loads skills from (verified
|
|
14
14
|
// against the 2026 docs), NOT necessarily a dir named after the tool:
|
|
15
15
|
// - Codex reads skills from .agents/skills (never .codex/skills); .codex is
|
|
16
|
-
// config-only. So codex shares the .agents layout with `
|
|
16
|
+
// config-only. So codex shares the .agents layout with `copilot` and
|
|
17
|
+
// `universal`.
|
|
17
18
|
// - ForgeCode reads skills from .forge/skills (never .forgecode/skills); its
|
|
18
19
|
// global dir is ~/forge (no dot) — see resolveBaseDir.
|
|
19
20
|
// - OpenCode reads project skills from .opencode; global is ~/.config/opencode
|
|
@@ -23,6 +24,7 @@ const forgeDir = path.join(pkgRoot, "forge");
|
|
|
23
24
|
const AGENTS = {
|
|
24
25
|
claude: ".claude",
|
|
25
26
|
codex: ".agents",
|
|
27
|
+
copilot: ".agents",
|
|
26
28
|
forgecode: ".forge",
|
|
27
29
|
opencode: ".opencode",
|
|
28
30
|
universal: ".agents",
|
|
@@ -53,16 +55,32 @@ function hasForgeSignature(baseDir) {
|
|
|
53
55
|
);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
const BANNER =
|
|
58
|
+
const BANNER = `██████╗ ██╗ █████╗ ██╗███╗ ██╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
57
59
|
██╔══██╗██║ ██╔══██╗██║████╗ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
|
|
58
60
|
██████╔╝██║ ███████║██║██╔██╗ ██║█████╗█████╗ ██║ ██║██████╔╝██║ ███╗█████╗
|
|
59
61
|
██╔═══╝ ██║ ██╔══██║██║██║╚██╗██║╚════╝██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
|
|
60
62
|
██║ ███████╗██║ ██║██║██║ ╚████║ ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
|
|
61
|
-
╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝
|
|
63
|
+
╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
|
62
64
|
`;
|
|
63
65
|
|
|
64
66
|
const TAGLINE = "turn ideas into ***plain specs.";
|
|
65
67
|
|
|
68
|
+
function terminalHasLightBackground(env = process.env) {
|
|
69
|
+
const parts = String(env.COLORFGBG ?? "").split(";");
|
|
70
|
+
const background = Number(parts.at(-1));
|
|
71
|
+
if (!Number.isInteger(background)) return false;
|
|
72
|
+
return background === 7 || background >= 9;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function terminalPalette(env = process.env) {
|
|
76
|
+
return terminalHasLightBackground(env)
|
|
77
|
+
? { brand: "76;92;0", plain: "0;105;55", link: "0;85;170" }
|
|
78
|
+
: { brand: "224;255;110", plain: "121;252;150", link: "95;175;255" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const color = (rgb, text, extra = "") =>
|
|
82
|
+
`\x1b[${extra}38;2;${rgb}m${text}\x1b[0m`;
|
|
83
|
+
|
|
66
84
|
function stripAnsi(s) {
|
|
67
85
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
68
86
|
}
|
|
@@ -75,13 +93,14 @@ function bannerWidth() {
|
|
|
75
93
|
|
|
76
94
|
function printBanner() {
|
|
77
95
|
if (!process.stdout.isTTY) return;
|
|
78
|
-
|
|
96
|
+
const palette = terminalPalette();
|
|
97
|
+
process.stdout.write("\n" + color(palette.brand, BANNER) + "\n");
|
|
79
98
|
|
|
80
99
|
const width = bannerWidth();
|
|
81
100
|
const pad = Math.max(0, Math.floor((width - TAGLINE.length) / 2));
|
|
82
101
|
const tag = TAGLINE.replace(
|
|
83
102
|
"***plain",
|
|
84
|
-
"
|
|
103
|
+
color(palette.plain, "***plain"),
|
|
85
104
|
);
|
|
86
105
|
process.stdout.write(" ".repeat(pad) + tag + "\n\n");
|
|
87
106
|
}
|
|
@@ -95,7 +114,7 @@ Commands:
|
|
|
95
114
|
uninstall Remove a plain-forge install using its manifest
|
|
96
115
|
|
|
97
116
|
Install options:
|
|
98
|
-
--agent <claude|codex|forgecode|opencode|universal>
|
|
117
|
+
--agent <claude|codex|copilot|forgecode|opencode|universal>
|
|
99
118
|
Target agent layout
|
|
100
119
|
--scope <project|global> Install into cwd or $HOME
|
|
101
120
|
-h, --help Show this help
|
|
@@ -105,7 +124,7 @@ Update options:
|
|
|
105
124
|
confirming each one
|
|
106
125
|
|
|
107
126
|
Uninstall options:
|
|
108
|
-
--agent <claude|codex|forgecode|opencode|universal|*>
|
|
127
|
+
--agent <claude|codex|copilot|forgecode|opencode|universal|*>
|
|
109
128
|
Which install to remove
|
|
110
129
|
(default: * — every agent)
|
|
111
130
|
--scope <project|global> Where to look (default: project)
|
|
@@ -462,10 +481,10 @@ function reportOpencodeUnmerge(result) {
|
|
|
462
481
|
}
|
|
463
482
|
}
|
|
464
483
|
|
|
465
|
-
// ---
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
484
|
+
// --- ForgeCode instructions wiring ------------------------------------------
|
|
485
|
+
// ForgeCode does not auto-read a rules/ directory; it loads custom instructions
|
|
486
|
+
// from AGENTS.md (repo AGENTS.md or ~/forge/AGENTS.md). AGENTS.md has no
|
|
487
|
+
// include/glob mechanism,
|
|
469
488
|
// so we append a managed pointer block that tells the agent to read our rules
|
|
470
489
|
// dir when touching .plain files. The block is fenced by markers so it can be
|
|
471
490
|
// refreshed or removed idempotently without disturbing the user's own content.
|
|
@@ -476,7 +495,7 @@ const AGENTS_MD_END = "<!-- END plain-forge (managed) -->";
|
|
|
476
495
|
// Agents whose rules are delivered through AGENTS.md rather than a natively
|
|
477
496
|
// auto-loaded rules/ dir (claude) or a config glob (opencode).
|
|
478
497
|
function usesAgentsMd(agent) {
|
|
479
|
-
return agent === "
|
|
498
|
+
return agent === "forgecode";
|
|
480
499
|
}
|
|
481
500
|
|
|
482
501
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -485,8 +504,10 @@ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
485
504
|
// root (cwd) for both tools; global scope differs per tool.
|
|
486
505
|
function agentsMdPath(agent, scope) {
|
|
487
506
|
if (scope === "project") return path.join(process.cwd(), "AGENTS.md");
|
|
488
|
-
|
|
489
|
-
|
|
507
|
+
// Retained only so update/uninstall can remove blocks created by older Codex
|
|
508
|
+
// installs. New Codex installs do not create this file.
|
|
509
|
+
if (agent === "codex") return path.join(os.homedir(), ".codex", "AGENTS.md");
|
|
510
|
+
return path.join(os.homedir(), "forge", "AGENTS.md");
|
|
490
511
|
}
|
|
491
512
|
|
|
492
513
|
// Glob the pointer block references — the installed rules dir. Project scope is
|
|
@@ -603,6 +624,10 @@ function wireRules(agent, scope) {
|
|
|
603
624
|
reportOpencodeMerge(mergeOpencodeInstructions(scope));
|
|
604
625
|
} else if (usesAgentsMd(agent)) {
|
|
605
626
|
reportAgentsMdMerge(mergeAgentsMd(agent, scope));
|
|
627
|
+
} else if (agent === "codex") {
|
|
628
|
+
// Migrate installs from the former Codex AGENTS.md wiring. Rules are now
|
|
629
|
+
// loaded by load-plain-reference itself.
|
|
630
|
+
reportAgentsMdUnmerge(unmergeAgentsMd(agent, scope));
|
|
606
631
|
}
|
|
607
632
|
} catch (err) {
|
|
608
633
|
warnRulesWiring("wire", agent, err);
|
|
@@ -616,7 +641,7 @@ function unwireRules(agent, scope) {
|
|
|
616
641
|
try {
|
|
617
642
|
if (agent === "opencode") {
|
|
618
643
|
reportOpencodeUnmerge(unmergeOpencodeInstructions(scope));
|
|
619
|
-
} else if (usesAgentsMd(agent)) {
|
|
644
|
+
} else if (usesAgentsMd(agent) || agent === "codex") {
|
|
620
645
|
reportAgentsMdUnmerge(unmergeAgentsMd(agent, scope));
|
|
621
646
|
}
|
|
622
647
|
} catch (err) {
|
|
@@ -655,8 +680,8 @@ function writeManifest(baseDir, files, agent) {
|
|
|
655
680
|
const target = manifestPathFor(baseDir);
|
|
656
681
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
657
682
|
// `agent` records which agent layout produced this install. It matters when
|
|
658
|
-
//
|
|
659
|
-
//
|
|
683
|
+
// Multiple agents resolve to the same .agents dir, so the manifest preserves
|
|
684
|
+
// the selected label for detection, updates, and uninstall output.
|
|
660
685
|
const manifest = {
|
|
661
686
|
name: "plain-forge",
|
|
662
687
|
version: readPkgVersion(),
|
|
@@ -714,7 +739,7 @@ function detectInstalls() {
|
|
|
714
739
|
for (const scope of SCOPES) {
|
|
715
740
|
for (const agent of Object.keys(AGENTS)) {
|
|
716
741
|
const baseDir = resolveBaseDir(agent, scope);
|
|
717
|
-
// Some agents share
|
|
742
|
+
// Some agents share the .agents directory.
|
|
718
743
|
// Only inspect each physical dir once so a single install isn't detected
|
|
719
744
|
// — and later updated/pruned — twice.
|
|
720
745
|
if (seen.has(baseDir)) continue;
|
|
@@ -929,8 +954,7 @@ async function cmdUninstall(args) {
|
|
|
929
954
|
);
|
|
930
955
|
|
|
931
956
|
// Undo the rules wiring (opencode.json / AGENTS.md). Keyed off the agent
|
|
932
|
-
// recorded at install time, not the requested name
|
|
933
|
-
// share the .agents dir, and only the codex install wired AGENTS.md. Done
|
|
957
|
+
// recorded at install time, not the requested name. Done
|
|
934
958
|
// before the dir cleanup so a now-orphaned config file living inside baseDir
|
|
935
959
|
// (e.g. forgecode-global ~/forge/AGENTS.md) can be removed and let baseDir
|
|
936
960
|
// itself be pruned.
|
|
@@ -960,11 +984,12 @@ async function cmdUninstall(args) {
|
|
|
960
984
|
}
|
|
961
985
|
|
|
962
986
|
function printNextSteps(agent) {
|
|
963
|
-
const
|
|
987
|
+
const palette = terminalPalette();
|
|
988
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
964
989
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
965
|
-
const plain = (s) =>
|
|
966
|
-
const codeplain = (s) =>
|
|
967
|
-
const link = (s) =>
|
|
990
|
+
const plain = (s) => color(palette.plain, s);
|
|
991
|
+
const codeplain = (s) => color(palette.brand, s);
|
|
992
|
+
const link = (s) => color(palette.link, s, "4;");
|
|
968
993
|
|
|
969
994
|
console.log(`\x1b[1mnext steps:\x1b[0m`);
|
|
970
995
|
console.log(
|
|
@@ -996,6 +1021,8 @@ function agentLabel(agent) {
|
|
|
996
1021
|
return "Claude Code";
|
|
997
1022
|
case "codex":
|
|
998
1023
|
return "Codex";
|
|
1024
|
+
case "copilot":
|
|
1025
|
+
return "GitHub Copilot";
|
|
999
1026
|
case "forgecode":
|
|
1000
1027
|
return "ForgeCode";
|
|
1001
1028
|
case "opencode":
|
|
@@ -1090,4 +1117,6 @@ export {
|
|
|
1090
1117
|
deleteForgeFile,
|
|
1091
1118
|
detectInstalls,
|
|
1092
1119
|
promptConfirm,
|
|
1120
|
+
terminalHasLightBackground,
|
|
1121
|
+
terminalPalette,
|
|
1093
1122
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Bullet and continuation syntax for every section in .plain files
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Rules for bullets and continuations in `.plain` files
|
|
6
|
+
|
|
7
|
+
These rules apply to every section and to concept explanations.
|
|
8
|
+
|
|
9
|
+
## No line-length limit
|
|
10
|
+
|
|
11
|
+
- `.plain` does not impose a maximum number of characters per line
|
|
12
|
+
- Do not split a valid line merely to satisfy an arbitrary formatting width
|
|
13
|
+
- Prefer clear, precise wording; concision must never remove required detail
|
|
14
|
+
|
|
15
|
+
## Never use bare continuation lines
|
|
16
|
+
|
|
17
|
+
- Every line inside a section must be a list item beginning with `- `
|
|
18
|
+
- An indented continuation without `- ` is invalid syntax
|
|
19
|
+
- If content is intentionally separated across lines, express the additional lines as nested
|
|
20
|
+
bullet items so each line remains syntactically valid
|
|
21
|
+
|
|
22
|
+
WRONG — bare continuation lines:
|
|
23
|
+
|
|
24
|
+
```plain
|
|
25
|
+
***functional specs***
|
|
26
|
+
|
|
27
|
+
- :GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(),
|
|
28
|
+
which returns a list of :EventEnvelope: dicts conforming to the gateway contract.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
GOOD — one valid list item:
|
|
32
|
+
|
|
33
|
+
```plain
|
|
34
|
+
***functional specs***
|
|
35
|
+
|
|
36
|
+
- :GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(), which returns a list of :EventEnvelope: dicts conforming to the gateway contract.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
BEST — separate clarifications expressed as nested bullets:
|
|
40
|
+
|
|
41
|
+
```plain
|
|
42
|
+
***functional specs***
|
|
43
|
+
|
|
44
|
+
- :GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle().
|
|
45
|
+
- The method returns a list of :EventEnvelope: dicts.
|
|
46
|
+
- The dicts conform to the gateway's :EventEnvelope: contract.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Presenting `.plain` examples
|
|
50
|
+
|
|
51
|
+
- Show every example under its owning section header
|
|
52
|
+
- Separate top-level list items with one blank line
|
|
53
|
+
- Keep nested clarifications directly under their parent without a blank line
|
|
54
|
+
- BAD, WRONG, `Before:`, and `Too complex:` examples may intentionally demonstrate invalid syntax
|
|
55
|
+
|
|
56
|
+
## Content that belongs in resources
|
|
57
|
+
|
|
58
|
+
Long URLs, schema fragments, and example payloads belong in `resources/` because they are external
|
|
59
|
+
artifacts, not because of their character count. Follow `linked-resources.md`.
|
|
@@ -42,14 +42,20 @@ When writing or editing a `***definitions***` section in a `.plain` file, always
|
|
|
42
42
|
Bad — circular:
|
|
43
43
|
|
|
44
44
|
```plain
|
|
45
|
+
***definitions***
|
|
46
|
+
|
|
45
47
|
- :Customer: is a user who has placed at least one :Order:.
|
|
48
|
+
|
|
46
49
|
- :Order: is placed by :Customer: and contains :OrderItem: entries.
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
`:Order:` references `:Customer:`, and `:Customer:` references `:Order:`. Fix by removing the back-reference:
|
|
50
53
|
|
|
51
54
|
```plain
|
|
55
|
+
***definitions***
|
|
56
|
+
|
|
52
57
|
- :Customer: is a user of the system.
|
|
58
|
+
|
|
53
59
|
- :Order: is placed by :Customer: and contains :OrderItem: entries.
|
|
54
60
|
```
|
|
55
61
|
|
|
@@ -69,13 +75,21 @@ Bad — circular:
|
|
|
69
75
|
Example of defining technical components alongside their behavior:
|
|
70
76
|
|
|
71
77
|
```plain
|
|
78
|
+
***definitions***
|
|
79
|
+
|
|
72
80
|
- :CsvToJsonConverter: bidirectionally converts csv and json strings.
|
|
81
|
+
|
|
73
82
|
- :CsvToJson: of :CsvToJsonConverter: converts a list of csv strings (e.g., "value1,value2,value3") to a list of json strings (e.g., "{\"key1\":\"value1\"}").
|
|
83
|
+
|
|
74
84
|
- :JsonToCsv: of :CsvToJsonConverter: converts a list of json strings (e.g., "{\"key1\":\"value1\"}") to a list of csv strings (e.g., "value1,value2").
|
|
75
85
|
|
|
76
86
|
- :StorageService: is an abstraction for a remote blob storage backend.
|
|
87
|
+
|
|
77
88
|
- :CsvFileWriter: writes CSV rows to local files, automatically rotates to new files when size limits are reached, and flushes completed files to :StorageService:.
|
|
89
|
+
|
|
78
90
|
- :CsvFileReader: reads CSV rows in a batched manner.
|
|
91
|
+
|
|
92
|
+
|
|
79
93
|
- :CsvJoiner: joins csvs and uses :CsvFileWriter: to create the merged CSV.
|
|
80
94
|
```
|
|
81
95
|
|
|
@@ -83,6 +97,7 @@ Example of defining technical components alongside their behavior:
|
|
|
83
97
|
|
|
84
98
|
```plain
|
|
85
99
|
***definitions***
|
|
100
|
+
|
|
86
101
|
- :ConceptName: is a description of what it represents.
|
|
87
102
|
- Attribute one (required)
|
|
88
103
|
- Attribute two (optional)
|
|
@@ -6,12 +6,53 @@ description: Rules for writing ***functional specs*** and ***acceptance tests***
|
|
|
6
6
|
|
|
7
7
|
When writing or editing a `***functional specs***` section in a `.plain` file, always follow these rules:
|
|
8
8
|
|
|
9
|
+
## Voice
|
|
10
|
+
- Write each functional spec as a **statement of fact** — present tense, indicative mood — describing what the software *is* or *does* once built, not as a command telling a developer what to build
|
|
11
|
+
- **Name a concrete subject**: an actor (`A :User: can …`), a named component or concept (`:PdfRenderer: lays out …`, `:Order: processing depends on …`), or the affected data in passive voice (`All :Resource: items are returned`)
|
|
12
|
+
- **Avoid the vague subject "The system …"** — it says nothing about *which* part does the work. Prefer the concrete component/concept, or passive voice with the affected concept as the subject
|
|
13
|
+
- **Never open a spec with an imperative** like "Implement …", "Add …", "Create …", "Show …", "Display …", "Build …" — those read as instructions, not specifications
|
|
14
|
+
- Do **not** use "should" / "should be able to" in a functional spec — that is requirement-modal voice; state the fact directly
|
|
15
|
+
|
|
16
|
+
BAD — imperative commands, or the vague "The system":
|
|
17
|
+
|
|
18
|
+
```plain
|
|
19
|
+
***functional specs***
|
|
20
|
+
|
|
21
|
+
- Implement the entry point for :App:.
|
|
22
|
+
|
|
23
|
+
- The system shows :TaskList:.
|
|
24
|
+
|
|
25
|
+
- Add :Task:.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
GOOD — statements of fact with concrete subjects:
|
|
29
|
+
|
|
30
|
+
```plain
|
|
31
|
+
***functional specs***
|
|
32
|
+
|
|
33
|
+
- :App: has an entry point.
|
|
34
|
+
|
|
35
|
+
- The :TaskList: is displayed.
|
|
36
|
+
|
|
37
|
+
- A :User: can add a :Task:. Only valid :Task: items are added.
|
|
38
|
+
```
|
|
39
|
+
|
|
9
40
|
## Complexity limit
|
|
10
41
|
- Each functional spec must imply a **maximum of 200 changed lines of code**
|
|
11
42
|
- If a spec is too large, use `break-down-func-spec` to split it into multiple smaller, independent specs
|
|
12
43
|
- Use `analyze-if-func-spec-too-complex` to verify before inserting
|
|
13
44
|
- Use `analyze-func-specs` to check a spec (or a batch of specs) against all relevant existing specs in a single batched call; use `resolve-spec-conflict` for each conflicting pair it reports
|
|
14
45
|
|
|
46
|
+
## First-spec bootstrap cost
|
|
47
|
+
- The first functional spec in a new root module carries implicit implementation cost even when its text describes little behavior
|
|
48
|
+
- The renderer may need to create the project structure, package metadata, framework configuration, entry point, initial source files, and unit-test infrastructure before implementing the behavior
|
|
49
|
+
- Count that bootstrap work toward the 200-changed-line limit; it is not free setup outside the spec
|
|
50
|
+
- Make the first spec the module's simplest useful, independently observable capability
|
|
51
|
+
- The first spec must still imply a testable outcome; setup alone is not a functional spec
|
|
52
|
+
- Prefer one minimal entry point or public operation with a deterministic result that can be invoked and verified
|
|
53
|
+
- Defer persistence, integrations, multiple interfaces, branching workflows, and cross-cutting behavior until later specs can build on the generated baseline
|
|
54
|
+
- This extra bootstrap cost applies when no previous implementation baseline exists; a module that continues from a `requires` module may already inherit the necessary scaffold
|
|
55
|
+
|
|
15
56
|
## Chronological ordering
|
|
16
57
|
- Specs are rendered incrementally, top to bottom
|
|
17
58
|
- The renderer has **no knowledge of future specs** — only previously rendered specs are in context
|
|
@@ -36,6 +77,14 @@ When writing or editing a `***functional specs***` section in a `.plain` file, a
|
|
|
36
77
|
- Nested lines clarify the parent spec — they do not introduce separate functionality
|
|
37
78
|
- Even with nested detail, the spec must still imply ≤ 200 lines of code
|
|
38
79
|
|
|
80
|
+
## Testability
|
|
81
|
+
- Every functional spec must describe behavior that can be verified through an observable interface
|
|
82
|
+
- State the triggering input or action and the deterministic outcome a test can observe
|
|
83
|
+
- Include relevant outputs, state changes, errors, or externally visible side effects
|
|
84
|
+
- Do not rely on generated implementation details as the only way to verify the behavior
|
|
85
|
+
- If a black-box conformance test cannot be derived from the spec, make the behavior more explicit
|
|
86
|
+
- Testability does not require an acceptance test; acceptance tests are reserved for full workflows
|
|
87
|
+
|
|
39
88
|
## Deterministic interface
|
|
40
89
|
- Specs must be detailed enough that a developer can use the built software without reading the generated code
|
|
41
90
|
- All external interfaces must be explicit: REST endpoint paths and HTTP methods, CLI command names and arguments, file formats, message schemas, etc.
|
|
@@ -46,10 +95,6 @@ When writing or editing a `***functional specs***` section in a `.plain` file, a
|
|
|
46
95
|
- `requires` modules only receive functional specs — do not rely on implementation reqs to convey behavior
|
|
47
96
|
- Behavior that downstream modules need must be expressed in functional specs, not elsewhere
|
|
48
97
|
|
|
49
|
-
## Line length
|
|
50
|
-
- See [`line-length.md`](line-length.md) — applies to every section, but bites hardest here because functional specs trend long
|
|
51
|
-
- 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)
|
|
52
|
-
|
|
53
98
|
## Acceptance tests
|
|
54
99
|
- Nest `***acceptance tests***` under a functional spec when verification criteria are needed
|
|
55
100
|
- 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
|
|
@@ -63,11 +108,11 @@ When writing or editing a `***functional specs***` section in a `.plain` file, a
|
|
|
63
108
|
```plain
|
|
64
109
|
***functional specs***
|
|
65
110
|
|
|
66
|
-
-
|
|
111
|
+
- :App: has an entry point.
|
|
67
112
|
|
|
68
|
-
- :User:
|
|
113
|
+
- A :User: can add a :Task:. Only valid :Task: items are added.
|
|
69
114
|
|
|
70
|
-
- :User:
|
|
115
|
+
- A :User: can send a :Message: to a :Conversation:.
|
|
71
116
|
- A :Message: must have non-empty content.
|
|
72
117
|
- The :Message: is appended to the end of the :Conversation:.
|
|
73
118
|
- All :Participant: members of the :Conversation: can see the new :Message:.
|
package/forge/rules/impl-reqs.md
CHANGED
|
@@ -13,7 +13,7 @@ When writing or editing an `***implementation reqs***` section in a `.plain` fil
|
|
|
13
13
|
|
|
14
14
|
## `:UnitTests:` lives here (hard rule)
|
|
15
15
|
- **Everything** about `:UnitTests:` goes in `***implementation reqs***` — paths, approach, packages, framework, conventions, fixtures, mocking policy, file layout, naming, lint / static-analysis gates
|
|
16
|
-
- `:UnitTests:` are part of the generated codebase (they sit inside `plain_modules/<module
|
|
16
|
+
- `:UnitTests:` are part of the generated codebase (they sit inside `plain_modules/<module>/code/` alongside the implementation), so requirements that shape them are implementation reqs by definition
|
|
17
17
|
- The unit-test generator reads **only** `***implementation reqs***` — anything about `:UnitTests:` placed elsewhere (e.g. `***test reqs***`) is silently ignored
|
|
18
18
|
- Author each `:UnitTests:` requirement via `add-implementation-requirement` and phrase it in terms of `:UnitTests:` so the partition stays visible at a glance
|
|
19
19
|
|
|
@@ -4,7 +4,10 @@ description: Rules for authoring the three test scripts that an embedded ***plai
|
|
|
4
4
|
|
|
5
5
|
# Rules for embedded-integration test scripts
|
|
6
6
|
|
|
7
|
-
When an embedded integration ships its three `test_scripts/` (prepare-environment, unit,
|
|
7
|
+
When an embedded integration ships its three `test_scripts/` (prepare-environment, unit,
|
|
8
|
+
conformance), these rules apply on top of the contracts in the corresponding
|
|
9
|
+
`implement-*-testing-script` skills. Use `load-plain-reference` for the rendering and testing
|
|
10
|
+
workflow overview when needed.
|
|
8
11
|
|
|
9
12
|
## Staging model (read this first)
|
|
10
13
|
|
|
@@ -72,7 +75,7 @@ mvn test -Dtest='<:UnitTests: package>.**.*Test' checkstyle:check
|
|
|
72
75
|
|
|
73
76
|
These reqs feed `run_conformance_tests_<lang>`. At minimum, declare:
|
|
74
77
|
|
|
75
|
-
1. **`:ConformanceTests:` source location** —
|
|
78
|
+
1. **`:ConformanceTests:` source location** — the generated suite lives under `plain_modules/<module>/tests/`, one folder per functional spec; the renderer passes the resolved path of the folder under test as `$2`
|
|
76
79
|
2. **`:ConformanceTests:` framework and execution command** — `mvn test --no-transfer-progress`, `pytest`, `npm test`, `go test ./...`, etc., with any flags / profiles the project requires
|
|
77
80
|
3. **Fully qualified `:ConformanceTests:` package** (or path / pattern) used to scope discovery, if the runner needs one
|
|
78
81
|
4. **`:ConformanceTests:` network and secrets policy** — by default the suite runs against the **live provider** (see [`integrations.md`](integrations.md) → *`:ConformanceTests:` always run against the live integration*). Declare the env-var names the script reads (e.g. `<PROVIDER>_API_KEY`), whether the script loads a `.env` file before running, and any specific endpoints that are mocked because they can't be exercised live safely (429, forced 5xx)
|
|
@@ -82,7 +85,7 @@ These reqs feed `run_conformance_tests_<lang>`. At minimum, declare:
|
|
|
82
85
|
Author the conformance facts as one or more entries, phrased in terms of `:ConformanceTests:`:
|
|
83
86
|
|
|
84
87
|
```plain
|
|
85
|
-
- :ConformanceTests: of :Implementation: live in `
|
|
88
|
+
- :ConformanceTests: of :Implementation: live in `plain_modules/foo/tests/` and are implemented with JUnit 5 + Maven.
|
|
86
89
|
- The fully qualified package used for :ConformanceTests: discovery is `com.example.integrations.foo.conformance`.
|
|
87
90
|
- :ConformanceTests: are run via `mvn test --no-transfer-progress`; the host's Surefire plugin must be installed.
|
|
88
91
|
- :ConformanceTests: run against the live :ProviderName: sandbox — no mocking of provider calls.
|
|
@@ -136,7 +139,7 @@ All three scripts are invoked by the `codeplain` renderer with positional argume
|
|
|
136
139
|
|
|
137
140
|
## 1. `prepare_environment_<lang>` — copy into the host, then compile
|
|
138
141
|
|
|
139
|
-
Receives one positional argument: the renderer's build output folder (e.g. `plain_modules/<module
|
|
142
|
+
Receives one positional argument: the renderer's build output folder (e.g. `plain_modules/<module>/code/`).
|
|
140
143
|
|
|
141
144
|
Purpose: stage the generated code **into the host codebase at the module's package path**, then run the host's install / build so that downstream test projects (specifically the conformance suite) can depend on it from the local dependency cache.
|
|
142
145
|
|
|
@@ -30,6 +30,9 @@ The embedded integration's `.plain` specs (and the rest of the ***plain project
|
|
|
30
30
|
│ ├── resources/
|
|
31
31
|
│ ├── test_scripts/
|
|
32
32
|
│ ├── plain_modules/ # generated; gitignored
|
|
33
|
+
│ │ └── <module>/
|
|
34
|
+
│ │ ├── code/ # implementation + unit tests
|
|
35
|
+
│ │ └── tests/ # conformance tests, per functional spec
|
|
33
36
|
│ └── config.yaml
|
|
34
37
|
└── src/ # host source tree (Java, Python, etc.)
|
|
35
38
|
```
|
|
@@ -117,7 +120,7 @@ The renderer reads the directives from the spec and the shapes from the linked s
|
|
|
117
120
|
|
|
118
121
|
## Test-script wiring — copy into the host, run tests there
|
|
119
122
|
|
|
120
|
-
Embedded integrations are tested **inside the host codebase itself**. The prepare and unit-test scripts copy the renderer's output (`$1`, i.e. `plain_modules/<module
|
|
123
|
+
Embedded integrations are tested **inside the host codebase itself**. The prepare and unit-test scripts copy the renderer's output (`$1`, i.e. `plain_modules/<module>/code/`) into the host's source tree at the module's package path, then compile / test the host project in place. Only the conformance script uses a scratch folder in the system temp directory (`/tmp/<lang>_conformance/`), because the conformance suite is a separate project that consumes the host build as a dependency.
|
|
121
124
|
|
|
122
125
|
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.
|
|
123
126
|
|
|
@@ -23,7 +23,7 @@ A linked resource **must not** be any of the following:
|
|
|
23
23
|
- 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
|
|
24
24
|
|
|
25
25
|
## Structured protocol artifacts must be linked, never transcribed
|
|
26
|
-
- 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
|
|
26
|
+
- 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 another directory within the project)
|
|
27
27
|
- 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
|
|
28
28
|
- 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
|
|
29
29
|
|
|
@@ -37,7 +37,7 @@ A linked resource **must not** be any of the following:
|
|
|
37
37
|
|
|
38
38
|
***functional specs***
|
|
39
39
|
|
|
40
|
-
- :User:
|
|
40
|
+
- A :User: can add a :Task: by POSTing :TaskCreateRequest: to the `POST /tasks` endpoint of :TasksAPI:.
|
|
41
41
|
- The endpoint responds per :TasksAPI:.
|
|
42
42
|
```
|
|
43
43
|
|
|
@@ -54,11 +54,12 @@ A linked resource **must not** be any of the following:
|
|
|
54
54
|
|
|
55
55
|
***functional specs***
|
|
56
56
|
|
|
57
|
-
- :User:
|
|
57
|
+
- A :User: can add a :Task: using :TaskModalSpec:.
|
|
58
58
|
|
|
59
|
-
- :User:
|
|
59
|
+
- A :User: can edit a :Task: using :TaskModalSpec:.
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
## File location and path resolution
|
|
63
|
-
- Paths are resolved relative to the
|
|
64
|
-
-
|
|
63
|
+
- Paths are resolved relative to the directory where `codeplain` is run (the current working directory)
|
|
64
|
+
- Relative paths may traverse into parent directories — `[resource](../../resources/resource.md)` is valid — as long as they resolve to a real text file on disk
|
|
65
|
+
- The conventional location is a `resources/` directory; keep the resource inside the project so the path stays stable from wherever `codeplain` is run
|
|
@@ -8,7 +8,7 @@ When creating or editing a `.plain` file that uses `requires`, always follow the
|
|
|
8
8
|
|
|
9
9
|
## What requires does
|
|
10
10
|
- `requires` establishes a **build ordering** — the required module is built before the current one
|
|
11
|
-
- The required module's generated code (`plain_modules/<required_module
|
|
11
|
+
- The required module's generated code (`plain_modules/<required_module>/code`) is copied as the starting point
|
|
12
12
|
- The required module's `***functional specs***` become visible as **previous functional specs** — this property **is transitive**
|
|
13
13
|
- Only `exported_concepts` from the required module are available — not its full definitions — and this property **is not transitive**
|
|
14
14
|
|
package/forge/rules/test-reqs.md
CHANGED
|
@@ -14,7 +14,7 @@ When writing or editing a `***test reqs***` section in a `.plain` file, always f
|
|
|
14
14
|
|
|
15
15
|
## `:ConformanceTests:` lives here (hard rule)
|
|
16
16
|
- **Everything** about `:ConformanceTests:` goes in `***test reqs***` — paths, approach, packages, framework, execution command, mocking / network policy, fixtures, pass criteria, environment prerequisites
|
|
17
|
-
- `:ConformanceTests:` live outside the generated codebase
|
|
17
|
+
- `:ConformanceTests:` live outside the generated codebase, in their own tree under `plain_modules/<module>/tests/` (one folder per functional spec), so requirements that shape them belong in test reqs by definition
|
|
18
18
|
- The conformance-test generator reads **only** `***test reqs***` — anything about `:ConformanceTests:` placed elsewhere (e.g. `***implementation reqs***`) is silently ignored
|
|
19
19
|
- Author each `:ConformanceTests:` requirement via `add-test-requirement` and phrase it in terms of `:ConformanceTests:` so the partition stays visible at a glance
|
|
20
20
|
- Conformance testing run scripts should be linked here as a linked resource
|