plain-forge 1.0.20 → 1.0.21-next.c19723e

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.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/bin/cli.mjs +1 -1
  3. package/forge/rules/bullet-continuation.md +16 -6
  4. package/forge/rules/concise-specs-examples.md +431 -0
  5. package/forge/rules/concise-specs-integration-examples.md +272 -0
  6. package/forge/rules/concise-specs.md +80 -0
  7. package/forge/rules/definitions.md +7 -4
  8. package/forge/rules/exported-concepts.md +3 -2
  9. package/forge/rules/func-specs.md +10 -3
  10. package/forge/rules/impl-reqs.md +8 -4
  11. package/forge/rules/import-modules.md +4 -6
  12. package/forge/rules/integration-embedded.md +0 -1
  13. package/forge/rules/integration-standalone.md +1 -1
  14. package/forge/rules/integrations.md +6 -7
  15. package/forge/rules/linked-resources.md +9 -8
  16. package/forge/rules/module-structure.md +105 -0
  17. package/forge/rules/required-concepts.md +5 -5
  18. package/forge/rules/requires-modules.md +37 -21
  19. package/forge/rules/test-reqs.md +11 -7
  20. package/forge/skills/add-acceptance-test/SKILL.md +1 -0
  21. package/forge/skills/add-concept/SKILL.md +2 -1
  22. package/forge/skills/add-feature/SKILL.md +2 -2
  23. package/forge/skills/add-functional-spec/SKILL.md +2 -1
  24. package/forge/skills/add-functional-specs/SKILL.md +2 -1
  25. package/forge/skills/add-implementation-requirement/SKILL.md +2 -1
  26. package/forge/skills/add-test-requirement/SKILL.md +1 -0
  27. package/forge/skills/break-down-func-spec/SKILL.md +1 -0
  28. package/forge/skills/check-forge-version/SKILL.md +154 -0
  29. package/forge/skills/check-forge-version/scripts/check-forge-version.mjs +368 -0
  30. package/forge/skills/create-import-module/SKILL.md +3 -3
  31. package/forge/skills/create-requires-module/SKILL.md +24 -19
  32. package/forge/skills/forge-plain/SKILL.md +1 -1
  33. package/forge/skills/forge-plain/references/checklist.md +2 -1
  34. package/forge/skills/forge-plain/references/phase-1-product.md +3 -2
  35. package/forge/skills/forge-plain/references/phase-2-tech.md +1 -1
  36. package/forge/skills/load-plain-reference/SKILL.md +20 -2
  37. package/forge/skills/load-plain-reference/references/project-model.md +0 -11
  38. package/forge/skills/plain-healthcheck/SKILL.md +1 -1
  39. package/forge/skills/refactor-module/SKILL.md +3 -3
  40. package/forge/skills/resolve-section-ownership/SKILL.md +65 -0
  41. package/forge/skills/resolve-spec-conflict/SKILL.md +1 -0
  42. package/package.json +4 -4
package/README.md CHANGED
@@ -262,6 +262,7 @@ On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them int
262
262
  | `analyze-func-specs` | Check a batch of specs (2+) against each other in one call and return every conflicting pair |
263
263
  | `analyze-2-func-specs` | Legacy: check exactly two specs for conflicts (prefer `analyze-func-specs`) |
264
264
  | `break-down-func-spec` | Split an overly complex spec into smaller specs (each ≤ 200 LOC) |
265
+ | `resolve-section-ownership` | Move each fact to the section that owns it — splits a bullet carrying several kinds of fact, and relocates a misplaced one the renderer is silently ignoring |
265
266
  | `resolve-spec-conflict` | Resolve a conflict between two functional specs |
266
267
 
267
268
  ### Debugging and Testing
package/bin/cli.mjs CHANGED
@@ -1010,7 +1010,7 @@ function printNextSteps(agent) {
1010
1010
  `prerequisite: install the ${codeplain("codeplain")} CLI to render your specs into code — ${link("https://www.codeplain.ai/")}`,
1011
1011
  );
1012
1012
  console.log(
1013
- `usage guide: ${link("https://github.com/Codeplain-ai/plain-forge#usage")}`,
1013
+ `usage guide: ${link("https://github.com/plainlang/plain-forge#usage")}`,
1014
1014
  );
1015
1015
  console.log();
1016
1016
  }
@@ -11,15 +11,25 @@ These rules apply to every section and to concept explanations.
11
11
  - `.plain` does not impose a maximum number of characters per line
12
12
  - Do not split a valid line merely to satisfy an arbitrary formatting width
13
13
  - Prefer clear, precise wording; concision must never remove required detail
14
+ - Length is bounded by content, not by width: say everything required, in the fewest plain words that stay exact (see `concise-specs.md`)
14
15
 
15
- ## Never use bare continuation lines
16
+ ## One statement per line
16
17
 
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
18
+ This is the preferred `.plain` authoring convention, not something the renderer enforces. A line
19
+ break in a section means one of exactly two things: a new `- ` statement, or a nested `- `
20
+ clarification of the statement above it. There is no third, typographic reason to break a line.
21
21
 
22
- WRONG — bare continuation lines:
22
+ - Write each statement on a single line, however long
23
+ - Never break a line just for width: a bare continuation (an indented line without `- `) adds a
24
+ line break that carries no structure — to every future reader and editing agent it is
25
+ indistinguishable from a deliberate sub-point
26
+ - The test: if content deserves its own line, it deserves its own `- ` bullet; if it doesn't,
27
+ keep it on the parent's line
28
+ - What the renderer does enforce is the top level: every statement in a section must be a `- `
29
+ list item — a flush-left prose line is a syntax error. A bare continuation, by contrast, is
30
+ accepted and folded into its parent item verbatim
31
+
32
+ BAD — line broken for width (bare continuation):
23
33
 
24
34
  ```plain
25
35
  ***functional specs***
@@ -0,0 +1,431 @@
1
+ ---
2
+ description: Worked BAD to GOOD examples of concise .plain specs, one pair per conciseness failure mode
3
+ ---
4
+
5
+ # Worked examples of concise `.plain` specs
6
+
7
+ `concise-specs.md` is the rule; this file is the example bank behind it. Every entry names one failure mode, shows the padded version, and shows the rewrite that carries the same information.
8
+
9
+ Read the pairs, not the prose: in each one, the GOOD block says everything the BAD block said, and usually pins down an interface detail the BAD block left open. Conciseness is what makes room for that detail — it is never a licence to drop it. The last section, *Do not over-cut*, shows the failure in the other direction.
10
+
11
+ The BAD blocks intentionally demonstrate the mistakes they name, including invalid syntax. Copy only from GOOD blocks.
12
+
13
+ Integration specs fail in their own ways — over-precision, inlined contract data, and edge cases the provider's documentation invites. Those examples are in `concise-specs-integration-examples.md`.
14
+
15
+ ## Padding around one fact
16
+
17
+ BAD:
18
+
19
+ ```plain
20
+ ***functional specs***
21
+
22
+ - In order to provide users with a robust and seamless experience, :TaskService: exposes a comprehensive endpoint for the creation of new :Task: items.
23
+
24
+ - The endpoint accepts a request and, provided that the incoming payload is valid and well-formed, proceeds to persist the :Task: appropriately.
25
+
26
+ - If the payload is not valid, an appropriate error is returned to the caller so that the user is informed of the problem.
27
+ ```
28
+
29
+ GOOD:
30
+
31
+ ```plain
32
+ ***functional specs***
33
+
34
+ - A :User: can create a :Task: via `POST /tasks`.
35
+ - The request body is a :Task: without `id`.
36
+ - The response is `201` with the created :Task:.
37
+ - A request without a name returns `400`.
38
+ ```
39
+
40
+ Three bullets became one, and the rewrite names the method, the path, the request shape, and both status codes — none of which the padded version pinned down.
41
+
42
+ ## Rationale, motivation, and benefits
43
+
44
+ BAD:
45
+
46
+ ```plain
47
+ ***functional specs***
48
+
49
+ - Because support staff frequently need to find customers quickly, and because scanning the full list is slow and error-prone, :CustomerDirectory: offers a search capability that greatly improves their daily workflow.
50
+ ```
51
+
52
+ GOOD:
53
+
54
+ ```plain
55
+ ***functional specs***
56
+
57
+ - :CustomerDirectory: returns :Customer: items whose name or email contains a search term, case-insensitively.
58
+ ```
59
+
60
+ Why the feature exists changes nothing about what is built. What "search" matches does.
61
+
62
+ ## Restating a concept's own name
63
+
64
+ BAD:
65
+
66
+ ```plain
67
+ ***definitions***
68
+
69
+ - :InvoiceNumber: is the invoice number of an :Invoice:. It is used to identify invoices.
70
+ ```
71
+
72
+ GOOD:
73
+
74
+ ```plain
75
+ ***definitions***
76
+
77
+ - :InvoiceNumber: identifies an :Invoice: within a :BillingPeriod: and has the form `INV-<year>-<6 digits>`.
78
+ ```
79
+
80
+ A definition that only expands the token into a sentence carries no information. State the scope of uniqueness and the format instead.
81
+
82
+ ## Stating the obvious
83
+
84
+ BAD:
85
+
86
+ ```plain
87
+ ***functional specs***
88
+
89
+ - :OrderApi: returns a list of :Order: items over HTTP.
90
+ - The list contains :Order: items.
91
+ - If there are no :Order: items, the list is empty.
92
+ - Each :Order: has an `id`, which is unique.
93
+ - The caller receives the response after the request is sent.
94
+ ```
95
+
96
+ GOOD:
97
+
98
+ ```plain
99
+ ***functional specs***
100
+
101
+ - :OrderApi: returns the :Order: items of the authenticated :User: via `GET /orders`, newest first.
102
+ - The response is `200` with a JSON array, empty when the :User: has no :Order: items.
103
+ ```
104
+
105
+ Every cut line was already implied: a list of :Order: items contains :Order: items, an identifier identifies, and a response follows a request. What survives is what a reader could *not* have guessed — the ownership filter, the sort order, and the response shape.
106
+
107
+ Apply the deletion test line by line: *if this line were missing, would anything be built differently?* "The list contains :Order: items" fails it. "Newest first" passes it.
108
+
109
+ ## Universal software defaults
110
+
111
+ BAD:
112
+
113
+ ```plain
114
+ ***functional specs***
115
+
116
+ - :OrderService: validates all incoming data before processing it.
117
+
118
+ - :OrderService: handles errors and returns meaningful error messages.
119
+
120
+ - :OrderService: saves the :Order: to storage so that it is not lost.
121
+ ```
122
+
123
+ GOOD:
124
+
125
+ ```plain
126
+ ***functional specs***
127
+
128
+ - :OrderService: rejects an :Order: whose `total` does not equal the sum of its :OrderLine: amounts, with `422`.
129
+ ```
130
+
131
+ Validating input, reporting errors, and persisting on a save operation belong in the `***implementation reqs***` and a baseline is assumed. If there is a specific use case for a particular operation, it should be documented there, not in the `***functional specs***`. The one fact worth stating is *which* rule is enforced and what the caller sees when it fails.
132
+
133
+ ## Standard behavior of a named technology
134
+
135
+ BAD:
136
+
137
+ ```plain
138
+ ***implementation reqs***
139
+
140
+ - :Implementation: uses FastAPI.
141
+
142
+ - FastAPI parses incoming HTTP requests and routes them to the correct handler function.
143
+
144
+ - Request and response bodies are serialized to and from JSON.
145
+
146
+ - :Implementation: uses SQLAlchemy, which opens connections to the database and maps rows to objects.
147
+ ```
148
+
149
+ GOOD:
150
+
151
+ ```plain
152
+ ***implementation reqs***
153
+
154
+ - :Implementation: is written in Python 3.12 and uses FastAPI.
155
+
156
+ - :Implementation: uses SQLAlchemy 2.x with a single session per request.
157
+ ```
158
+
159
+ Once a technology is named, its documented behavior comes with it. Keep only the choices a reader could not derive from the name — here, the version and the session scope.
160
+
161
+ ## Uncheckable adjectives and adverbs
162
+
163
+ BAD:
164
+
165
+ ```plain
166
+ ***implementation reqs***
167
+
168
+ - :Implementation: handles large files efficiently and gracefully, following modern best practices.
169
+
170
+ - :UnitTests: provide comprehensive coverage of all important functionality.
171
+ ```
172
+
173
+ GOOD:
174
+
175
+ ```plain
176
+ ***implementation reqs***
177
+
178
+ - :FileDownloader: streams a file larger than 10 MB to disk instead of holding it in memory.
179
+
180
+ - :UnitTests: of :FileDownloader: serve fixture files from a local HTTP server, never a live host.
181
+ ```
182
+
183
+ `modern`, `best practices`, `efficiently`, `gracefully`, and `comprehensive` fail the same test: no test can distinguish a build that has them from one that does not. Either name the checkable threshold or delete the word.
184
+
185
+ Note the subject too. `:Implementation:` is the whole codebase — right for a fact that holds everywhere, too coarse for a rule about one behavior, and a requirement no reader can locate is one no reviewer can check. Name the component the requirement constrains.
186
+
187
+ The coverage line has no checkable version worth writing: testing every branch is what `:UnitTests:` are for, so "comprehensive coverage" converts to nothing. Not every uncheckable line becomes a checkable one — some are cut and replaced by the fact the section was missing, here the one thing nobody can derive: where the bytes come from when the tests run.
188
+
189
+ ## Hedging
190
+
191
+ BAD:
192
+
193
+ ```plain
194
+ ***test reqs***
195
+
196
+ - :ConformanceTests: should ideally use pytest, or another suitable framework if that is more appropriate.
197
+
198
+ - :ConformanceTests: may optionally mock external HTTP calls where possible.
199
+ ```
200
+
201
+ GOOD:
202
+
203
+ ```plain
204
+ ***test reqs***
205
+
206
+ - :ConformanceTests: are implemented with pytest.
207
+
208
+ - :ConformanceTests: mock every external HTTP call.
209
+ ```
210
+
211
+ Every hedge hands the renderer a decision the spec was supposed to make. A spec states one
212
+ behavior.
213
+
214
+ ## Ornate vocabulary
215
+
216
+ BAD:
217
+
218
+ ```plain
219
+ ***functional specs***
220
+
221
+ - :ReportGenerator: is responsible for facilitating the utilization of a plurality of :DataSource: instances in order to initiate the generation of a :Report: prior to the performance of the validation of the resulting output.
222
+ ```
223
+
224
+ GOOD:
225
+
226
+ ```plain
227
+ ***functional specs***
228
+
229
+ - :ReportGenerator: builds a :Report: from one or more :DataSource: items, then validates it.
230
+ ```
231
+
232
+ Same sequence, plain words. See the substitution table in `concise-specs.md`.
233
+
234
+ ## Sub-bullets that elaborate instead of disambiguating
235
+
236
+ BAD:
237
+
238
+ ```plain
239
+ ***functional specs***
240
+
241
+ - A :User: can archive a :Project:.
242
+ - Archiving is an action the user performs on a project.
243
+ - The archive action is available to users who have access to the project.
244
+ - Once archived, the project is in the archived state.
245
+ ```
246
+
247
+ GOOD:
248
+
249
+ ```plain
250
+ ***functional specs***
251
+
252
+ - A :User: can archive a :Project: they own.
253
+ - An archived :Project: is excluded from :ProjectList:.
254
+ - Archiving a :Project: that is already archived returns `409`.
255
+ ```
256
+
257
+ A sub-bullet earns its line by removing an ambiguity. If it only says the parent again in more words, it is filler.
258
+
259
+ ## Restating the parent bullet
260
+
261
+ BAD:
262
+
263
+ ```plain
264
+ ***functional specs***
265
+
266
+ - :SessionStore: expires a :Session: 30 minutes after its last use.
267
+ - Sessions do not live forever; they time out.
268
+ - The timeout is 30 minutes.
269
+ ```
270
+
271
+ GOOD:
272
+
273
+ ```plain
274
+ ***functional specs***
275
+
276
+ - :SessionStore: expires a :Session: 30 minutes after its last use.
277
+ - A request carrying an expired :Session: returns `401`.
278
+ ```
279
+
280
+ ## Synonym drift
281
+
282
+ BAD:
283
+
284
+ ```plain
285
+ ***functional specs***
286
+
287
+ - A :User: can upload a :Document:.
288
+
289
+ - The system stores the submitted file in :DocumentStore:.
290
+
291
+ - The uploaded asset is retrievable by its identifier.
292
+ ```
293
+
294
+ Three names — `:Document:`, "file", "asset" — read as three things. A reader cannot tell whether "asset" is a fourth concept, and the renderer cannot either.
295
+
296
+ GOOD:
297
+
298
+ ```plain
299
+ ***functional specs***
300
+
301
+ - A :User: can upload a :Document:.
302
+ - :DocumentStore: stores the :Document: and assigns it a :DocumentId:.
303
+ - A :User: can retrieve a :Document: by its :DocumentId:.
304
+ ```
305
+
306
+ ## Meta-commentary and scope disclaimers
307
+
308
+ BAD:
309
+
310
+ ```plain
311
+ ***functional specs***
312
+
313
+ - This spec is intentionally kept small so that it stays within the complexity limit.
314
+
315
+ - As described above, :ExportService: writes a :CsvFile:.
316
+
317
+ - No changes are made to the existing :ImportService: or to the user interface.
318
+
319
+ - Future specs will extend this behavior with additional formats.
320
+ ```
321
+
322
+ GOOD:
323
+
324
+ ```plain
325
+ ***functional specs***
326
+
327
+ - :ExportService: writes the :Order: items of a :BillingPeriod: to a :CsvFile:.
328
+ - The header row is `id,customer,total,currency`.
329
+ - An empty :BillingPeriod: produces a :CsvFile: with only the header row.
330
+ ```
331
+
332
+ A spec describes the software, not itself. What the spec does *not* change is everything it does not mention, and what later specs will add belongs to those specs.
333
+
334
+ ## Prose and tables inside a section
335
+
336
+ BAD:
337
+
338
+ ```plain
339
+ ***functional specs***
340
+
341
+ - :PricingEngine: applies discounts.
342
+ The discount matrix is as follows:
343
+ | Tier | Volume | Discount |
344
+ |---|---|---|
345
+ | Bronze | 0-99 | 0% |
346
+ | Silver | 100-499 | 5% |
347
+ | Gold | 500+ | 12% |
348
+ ```
349
+
350
+ GOOD:
351
+
352
+ ```plain
353
+ ***functional specs***
354
+
355
+ - :PricingEngine: applies the :DiscountTier: matrix in [resources/discount-tiers.json](resources/discount-tiers.json) to an :Order: total.
356
+ - An :Order: below the lowest tier threshold gets no discount.
357
+ ```
358
+
359
+ A section holds `- ` list items. Matrices, schemas, and payloads are external artifacts and are linked from `resources/` — see `linked-resources.md`.
360
+
361
+ ## Split one crowded bullet across its owning sections
362
+
363
+ One bullet that mixes behavior, technology, and test policy is hard to review and partly ignored: the renderer reads each concept only from its owning section, so the unit-test and conformance-test facts below never reach the generators that need them.
364
+
365
+ BAD — everything in one place:
366
+
367
+ ```plain
368
+ ***definitions***
369
+
370
+ - :ImageResizer: resizes images.
371
+
372
+ - :Image: is an image file uploaded by the user.
373
+
374
+ ***functional specs***
375
+
376
+ - :ImageResizer:, implemented as a Python class using Pillow with a thread pool of 4 workers, resizes an uploaded :Image: to a thumbnail, where a thumbnail is defined as a JPEG at most 256 pixels on its longest edge.
377
+ ```
378
+
379
+ GOOD — each fact in the section that owns it:
380
+
381
+ ```plain
382
+ ***definitions***
383
+
384
+ - :ImageResizer: resizes images.
385
+
386
+ - :Image: is an image file uploaded by the user.
387
+
388
+ - :Thumbnail: is a JPEG derived from an :Image:, at most 256 pixels on its longest edge, with the aspect ratio preserved.
389
+
390
+ - :UnsupportedImageError: is raised when an :Image: is not a supported format.
391
+
392
+ ***implementation reqs***
393
+
394
+ - :ImageResizer: resizes images with Pillow, on a thread pool of 4 workers.
395
+
396
+ ***functional specs***
397
+
398
+ - :ImageResizer: produces a :Thumbnail: from an :Image:.
399
+ - An :Image: already within the :Thumbnail: bounds is returned unchanged.
400
+ - An :Image: that is not a supported format raises :UnsupportedImageError:.
401
+ ```
402
+
403
+ Each section now answers one question, and the definition, the mechanism, and the behavior are each one line to check instead of clauses buried in a paragraph about resizing. Use the split whenever a bullet answers more than one of *what is it*, *what does it do*, *how is it built*, and *how is it tested* — see `module-structure.md` for section ownership and canonical section order.
404
+
405
+ Use `resolve-section-ownership` to perform the split. It is not the same as splitting a functional spec that is simply too large — for that, run `analyze-if-func-spec-too-complex`, then `break-down-func-spec`.
406
+
407
+ The most common single case of this is a functional spec that states a mechanism rather than a result — a retry schedule, a cache, a token refresh. `impl-reqs.md` owns HOW; a functional spec keeps what a caller observes. `concise-specs-integration-examples.md` works through several.
408
+
409
+ ## Narrated acceptance tests
410
+
411
+ BAD:
412
+
413
+ ```plain
414
+ ***acceptance tests***
415
+
416
+ - The tester should first open the application and log in with a valid account, then navigate to the projects area where they will be able to see the list of projects, and then they should create a new project by filling in the form, after which they should verify that everything worked as expected and the project shows up correctly.
417
+ ```
418
+
419
+ GOOD:
420
+
421
+ ```plain
422
+ ***acceptance tests***
423
+
424
+ - A :User: creates a :Project: and finds it in :ProjectList:.
425
+ - Sign in as a :User: with no :Project: items.
426
+ - Create a :Project: named `Apollo`.
427
+ - :ProjectList: contains exactly one :Project:, named `Apollo`.
428
+ ```
429
+
430
+ One narrated paragraph becomes an ordered set of checkable steps, and "everything worked as
431
+ expected" becomes an assertion.