plain-forge 1.0.18 → 1.0.19
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 +15 -8
- 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/integration-embedded-testing.md +4 -1
- package/forge/rules/linked-resources.md +7 -6
- 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 +2 -1
- 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/init-config-file/SKILL.md +2 -0
- 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 +47 -0
- package/forge/skills/load-plain-reference/references/rendering-and-testing.md +57 -0
- package/forge/skills/plain-healthcheck/SKILL.md +2 -2
- package/forge/skills/resolve-spec-conflict/SKILL.md +13 -5
- package/forge/skills/run-codeplain/SKILL.md +1 -1
- package/package.json +1 -1
- package/forge/rules/line-length.md +0 -45
package/README.md
CHANGED
|
@@ -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`.
|
|
@@ -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:.
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -32,10 +32,11 @@ Acceptance tests are nested under the functional spec they verify, using a `***a
|
|
|
32
32
|
```plain
|
|
33
33
|
***functional specs***
|
|
34
34
|
|
|
35
|
-
-
|
|
35
|
+
- :Task: items are processed in batches of 100.
|
|
36
36
|
|
|
37
37
|
***acceptance tests***
|
|
38
38
|
- Processing 250 :Task: items should result in 3 batches.
|
|
39
|
+
|
|
39
40
|
- Each batch should contain at most 100 items.
|
|
40
41
|
```
|
|
41
42
|
|
|
@@ -44,29 +45,6 @@ Key formatting rules:
|
|
|
44
45
|
- Each test bullet is indented under the `***acceptance tests***` header.
|
|
45
46
|
- Multiple acceptance tests can be listed under a single functional spec.
|
|
46
47
|
|
|
47
|
-
### Line syntax (hard rule)
|
|
48
|
-
|
|
49
|
-
**Every line inside `***acceptance tests***` must be its own list item starting with `- `.** ***plain has no concept of bare continuation lines — indented prose without a leading `- ` is **invalid syntax** and the renderer will reject it.
|
|
50
|
-
|
|
51
|
-
- Hard limit: 120 characters per line. If a sentence is too long, **split it at a natural clause boundary into nested `- ` bullets** — never wrap onto an unprefixed line.
|
|
52
|
-
- Nested clarifications are also `- ` items, indented under the parent. The indentation alone is not enough; the leading `- ` is required.
|
|
53
|
-
|
|
54
|
-
BAD — bare continuation lines (invalid ***plain syntax, will not render):
|
|
55
|
-
|
|
56
|
-
```plain
|
|
57
|
-
***acceptance tests***
|
|
58
|
-
- Processing 250 :Task: items should result in 3 batches with the
|
|
59
|
-
last batch containing the remaining 50 items.
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
GOOD — every line starts with `- `:
|
|
63
|
-
|
|
64
|
-
```plain
|
|
65
|
-
***acceptance tests***
|
|
66
|
-
- Processing 250 :Task: items should result in 3 batches.
|
|
67
|
-
- The first two batches each contain 100 items.
|
|
68
|
-
- The last batch contains the remaining 50 items.
|
|
69
|
-
```
|
|
70
48
|
|
|
71
49
|
## Rules
|
|
72
50
|
|
|
@@ -76,7 +54,7 @@ An acceptance test is essentially an **example that illustrates** the functional
|
|
|
76
54
|
|
|
77
55
|
```
|
|
78
56
|
Functional spec:
|
|
79
|
-
-
|
|
57
|
+
- :Resource: items are returned sorted by creation date in descending order.
|
|
80
58
|
|
|
81
59
|
Good (consistent):
|
|
82
60
|
- The first :Resource: in the response should have the most recent creation date.
|
|
@@ -44,40 +44,21 @@ A concept definition is a bullet in `***definitions***` that starts with the con
|
|
|
44
44
|
|
|
45
45
|
```plain
|
|
46
46
|
***definitions***
|
|
47
|
+
|
|
47
48
|
- :ConceptName: is a description of what it represents.
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
Attributes and constraints are nested sub-bullets:
|
|
51
52
|
|
|
52
53
|
```plain
|
|
54
|
+
***definitions***
|
|
55
|
+
|
|
53
56
|
- :Task: describes an activity that needs to be done by :User:. :Task: has:
|
|
54
57
|
- Name - a short description (required)
|
|
55
58
|
- Notes - additional details (optional)
|
|
56
59
|
- Due Date - completion deadline (optional)
|
|
57
60
|
```
|
|
58
61
|
|
|
59
|
-
## Line syntax (hard rule)
|
|
60
|
-
|
|
61
|
-
**Every line inside `***definitions***` must be its own list item starting with `- `.** ***plain has no concept of bare continuation lines — indented prose without a leading `- ` is **invalid syntax** and the renderer will reject it.
|
|
62
|
-
|
|
63
|
-
- Hard limit: 120 characters per line. If a sentence is too long, **split it at a natural clause boundary into nested `- ` bullets** — never wrap onto an unprefixed line.
|
|
64
|
-
- Nested attributes are also `- ` items, indented under the parent. The indentation alone is not enough; the leading `- ` is required.
|
|
65
|
-
|
|
66
|
-
BAD — bare continuation lines (invalid ***plain syntax, will not render):
|
|
67
|
-
|
|
68
|
-
```plain
|
|
69
|
-
- :Task: describes an activity that needs to be done by :User:.
|
|
70
|
-
- Name is a short description that the user provides when creating
|
|
71
|
-
the task and is shown in the task list.
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
GOOD — every line starts with `- `:
|
|
75
|
-
|
|
76
|
-
```plain
|
|
77
|
-
- :Task: describes an activity that needs to be done by :User:.
|
|
78
|
-
- Name is a short description provided when creating the task.
|
|
79
|
-
- The name is shown in the task list.
|
|
80
|
-
```
|
|
81
62
|
|
|
82
63
|
## Validation Checklist
|
|
83
64
|
|