plain-forge 1.0.23 → 1.0.24-next.019bba8

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 (61) hide show
  1. package/README.md +14 -11
  2. package/bin/cli.mjs +5 -4
  3. package/forge/rules/plain-forge.md +10 -0
  4. package/forge/skills/add-concept/SKILL.md +1 -1
  5. package/forge/skills/add-feature/SKILL.md +8 -7
  6. package/forge/skills/add-feature/references/checklist.md +2 -2
  7. package/forge/skills/add-functional-spec/SKILL.md +14 -10
  8. package/forge/skills/add-functional-specs/SKILL.md +14 -10
  9. package/forge/skills/add-implementation-requirement/SKILL.md +1 -1
  10. package/forge/skills/add-resource/SKILL.md +3 -2
  11. package/forge/skills/add-template/SKILL.md +1 -1
  12. package/forge/skills/add-test-requirement/SKILL.md +4 -4
  13. package/forge/skills/add-test-scenarios/SKILL.md +98 -0
  14. package/forge/skills/analyze-2-func-specs/SKILL.md +1 -1
  15. package/forge/skills/analyze-func-specs/SKILL.md +2 -2
  16. package/forge/skills/analyze-if-func-spec-too-complex/SKILL.md +44 -24
  17. package/forge/skills/break-down-func-spec/SKILL.md +57 -33
  18. package/forge/skills/check-plain-env/SKILL.md +2 -2
  19. package/forge/skills/consolidate-concepts/SKILL.md +8 -8
  20. package/forge/skills/create-import-module/SKILL.md +1 -1
  21. package/forge/skills/create-requires-module/SKILL.md +6 -3
  22. package/forge/skills/debug-specs/SKILL.md +14 -8
  23. package/forge/skills/forge-plain/SKILL.md +3 -3
  24. package/forge/skills/forge-plain/references/checklist.md +3 -3
  25. package/forge/skills/forge-plain/references/phase-1-product.md +2 -2
  26. package/forge/skills/forge-plain/references/phase-2-tech.md +1 -1
  27. package/forge/skills/forge-plain/references/phase-3-testing.md +5 -5
  28. package/forge/skills/init-config-file/SKILL.md +2 -2
  29. package/forge/skills/init-plain-project/SKILL.md +6 -6
  30. package/forge/skills/load-plain-reference/SKILL.md +25 -25
  31. package/forge/skills/load-plain-reference/references/project-model.md +2 -1
  32. package/forge/skills/load-plain-reference/references/rendering-and-testing.md +7 -5
  33. package/forge/{rules → skills/load-plain-reference/rules}/bullet-continuation.md +11 -11
  34. package/forge/{rules → skills/load-plain-reference/rules}/concise-specs-examples.md +100 -69
  35. package/forge/{rules → skills/load-plain-reference/rules}/concise-specs-integration-examples.md +39 -24
  36. package/forge/{rules → skills/load-plain-reference/rules}/concise-specs.md +3 -2
  37. package/forge/{rules → skills/load-plain-reference/rules}/func-specs.md +37 -28
  38. package/forge/{rules → skills/load-plain-reference/rules}/impl-reqs.md +1 -1
  39. package/forge/{rules → skills/load-plain-reference/rules}/integration-embedded-testing.md +1 -1
  40. package/forge/{rules → skills/load-plain-reference/rules}/linked-resources.md +7 -4
  41. package/forge/{rules → skills/load-plain-reference/rules}/module-structure.md +16 -9
  42. package/forge/{rules → skills/load-plain-reference/rules}/requires-modules.md +1 -0
  43. package/forge/skills/load-plain-reference/rules/semantic-ids.md +47 -0
  44. package/forge/skills/load-plain-reference/rules/test-plain-files.md +67 -0
  45. package/forge/{rules → skills/load-plain-reference/rules}/test-reqs.md +6 -6
  46. package/forge/skills/plain-healthcheck/SKILL.md +31 -13
  47. package/forge/skills/refactor-module/SKILL.md +16 -16
  48. package/forge/skills/resolve-section-ownership/SKILL.md +3 -3
  49. package/forge/skills/resolve-spec-conflict/SKILL.md +14 -8
  50. package/package.json +1 -1
  51. package/forge/skills/add-acceptance-test/SKILL.md +0 -101
  52. /package/forge/{rules → skills/load-plain-reference/rules}/definitions.md +0 -0
  53. /package/forge/{rules → skills/load-plain-reference/rules}/examples/integration-embedded-testing/prepare_environment_java.sh +0 -0
  54. /package/forge/{rules → skills/load-plain-reference/rules}/examples/integration-embedded-testing/run_conformance_tests_java.sh +0 -0
  55. /package/forge/{rules → skills/load-plain-reference/rules}/examples/integration-embedded-testing/run_unittests_java.sh +0 -0
  56. /package/forge/{rules → skills/load-plain-reference/rules}/exported-concepts.md +0 -0
  57. /package/forge/{rules → skills/load-plain-reference/rules}/import-modules.md +0 -0
  58. /package/forge/{rules → skills/load-plain-reference/rules}/integration-embedded.md +0 -0
  59. /package/forge/{rules → skills/load-plain-reference/rules}/integration-standalone.md +0 -0
  60. /package/forge/{rules → skills/load-plain-reference/rules}/integrations.md +0 -0
  61. /package/forge/{rules → skills/load-plain-reference/rules}/required-concepts.md +0 -0
@@ -19,11 +19,14 @@ BAD:
19
19
  ```plain
20
20
  ***functional specs***
21
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.
22
+ # create-task
23
+ 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
 
24
- - The endpoint accepts a request and, provided that the incoming payload is valid and well-formed, proceeds to persist the :Task: appropriately.
25
+ # persist-task
26
+ The endpoint accepts a request and, provided that the incoming payload is valid and well-formed, proceeds to persist the :Task: appropriately.
25
27
 
26
- - If the payload is not valid, an appropriate error is returned to the caller so that the user is informed of the problem.
28
+ # reject-invalid-payload
29
+ If the payload is not valid, an appropriate error is returned to the caller so that the user is informed of the problem.
27
30
  ```
28
31
 
29
32
  GOOD:
@@ -31,10 +34,11 @@ GOOD:
31
34
  ```plain
32
35
  ***functional specs***
33
36
 
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`.
37
+ # create-task
38
+ A :User: can create a :Task: via `POST /tasks`.
39
+ - The request body is a :Task: without `id`.
40
+ - The response is `201` with the created :Task:.
41
+ - A request without a name returns `400`.
38
42
  ```
39
43
 
40
44
  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.
@@ -46,7 +50,8 @@ BAD:
46
50
  ```plain
47
51
  ***functional specs***
48
52
 
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.
53
+ # search-customers
54
+ 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
55
  ```
51
56
 
52
57
  GOOD:
@@ -54,7 +59,8 @@ GOOD:
54
59
  ```plain
55
60
  ***functional specs***
56
61
 
57
- - :CustomerDirectory: returns :Customer: items whose name or email contains a search term, case-insensitively.
62
+ # search-customers
63
+ :CustomerDirectory: returns :Customer: items whose name or email contains a search term, case-insensitively.
58
64
  ```
59
65
 
60
66
  Why the feature exists changes nothing about what is built. What "search" matches does.
@@ -86,11 +92,12 @@ BAD:
86
92
  ```plain
87
93
  ***functional specs***
88
94
 
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.
95
+ # list-orders
96
+ :OrderApi: returns a list of :Order: items over HTTP.
97
+ - The list contains :Order: items.
98
+ - If there are no :Order: items, the list is empty.
99
+ - Each :Order: has an `id`, which is unique.
100
+ - The caller receives the response after the request is sent.
94
101
  ```
95
102
 
96
103
  GOOD:
@@ -98,8 +105,9 @@ GOOD:
98
105
  ```plain
99
106
  ***functional specs***
100
107
 
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.
108
+ # list-orders
109
+ :OrderApi: returns the :Order: items of the authenticated :User: via `GET /orders`, newest first.
110
+ - The response is `200` with a JSON array, empty when the :User: has no :Order: items.
103
111
  ```
104
112
 
105
113
  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.
@@ -113,11 +121,14 @@ BAD:
113
121
  ```plain
114
122
  ***functional specs***
115
123
 
116
- - :OrderService: validates all incoming data before processing it.
124
+ # validate-input
125
+ :OrderService: validates all incoming data before processing it.
117
126
 
118
- - :OrderService: handles errors and returns meaningful error messages.
127
+ # handle-errors
128
+ :OrderService: handles errors and returns meaningful error messages.
119
129
 
120
- - :OrderService: saves the :Order: to storage so that it is not lost.
130
+ # save-order
131
+ :OrderService: saves the :Order: to storage so that it is not lost.
121
132
  ```
122
133
 
123
134
  GOOD:
@@ -125,7 +136,8 @@ GOOD:
125
136
  ```plain
126
137
  ***functional specs***
127
138
 
128
- - :OrderService: rejects an :Order: whose `total` does not equal the sum of its :OrderLine: amounts, with `422`.
139
+ # reject-wrong-total
140
+ :OrderService: rejects an :Order: whose `total` does not equal the sum of its :OrderLine: amounts, with `422`.
129
141
  ```
130
142
 
131
143
  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.
@@ -218,7 +230,8 @@ BAD:
218
230
  ```plain
219
231
  ***functional specs***
220
232
 
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.
233
+ # build-report
234
+ :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
235
  ```
223
236
 
224
237
  GOOD:
@@ -226,7 +239,8 @@ GOOD:
226
239
  ```plain
227
240
  ***functional specs***
228
241
 
229
- - :ReportGenerator: builds a :Report: from one or more :DataSource: items, then validates it.
242
+ # build-report
243
+ :ReportGenerator: builds a :Report: from one or more :DataSource: items, then validates it.
230
244
  ```
231
245
 
232
246
  Same sequence, plain words. See the substitution table in `concise-specs.md`.
@@ -238,10 +252,11 @@ BAD:
238
252
  ```plain
239
253
  ***functional specs***
240
254
 
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.
255
+ # archive-project
256
+ A :User: can archive a :Project:.
257
+ - Archiving is an action the user performs on a project.
258
+ - The archive action is available to users who have access to the project.
259
+ - Once archived, the project is in the archived state.
245
260
  ```
246
261
 
247
262
  GOOD:
@@ -249,9 +264,10 @@ GOOD:
249
264
  ```plain
250
265
  ***functional specs***
251
266
 
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`.
267
+ # archive-project
268
+ A :User: can archive a :Project: they own.
269
+ - An archived :Project: is excluded from :ProjectList:.
270
+ - Archiving a :Project: that is already archived returns `409`.
255
271
  ```
256
272
 
257
273
  A sub-bullet earns its line by removing an ambiguity. If it only says the parent again in more words, it is filler.
@@ -263,9 +279,10 @@ BAD:
263
279
  ```plain
264
280
  ***functional specs***
265
281
 
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.
282
+ # expire-session
283
+ :SessionStore: expires a :Session: 30 minutes after its last use.
284
+ - Sessions do not live forever; they time out.
285
+ - The timeout is 30 minutes.
269
286
  ```
270
287
 
271
288
  GOOD:
@@ -273,8 +290,9 @@ GOOD:
273
290
  ```plain
274
291
  ***functional specs***
275
292
 
276
- - :SessionStore: expires a :Session: 30 minutes after its last use.
277
- - A request carrying an expired :Session: returns `401`.
293
+ # expire-session
294
+ :SessionStore: expires a :Session: 30 minutes after its last use.
295
+ - A request carrying an expired :Session: returns `401`.
278
296
  ```
279
297
 
280
298
  ## Synonym drift
@@ -284,11 +302,14 @@ BAD:
284
302
  ```plain
285
303
  ***functional specs***
286
304
 
287
- - A :User: can upload a :Document:.
305
+ # upload-document
306
+ A :User: can upload a :Document:.
288
307
 
289
- - The system stores the submitted file in :DocumentStore:.
308
+ # store-document
309
+ The system stores the submitted file in :DocumentStore:.
290
310
 
291
- - The uploaded asset is retrievable by its identifier.
311
+ # retrieve-document
312
+ The uploaded asset is retrievable by its identifier.
292
313
  ```
293
314
 
294
315
  Three names — `:Document:`, "file", "asset" — read as three things. A reader cannot tell whether "asset" is a fourth concept, and the renderer cannot either.
@@ -298,9 +319,10 @@ GOOD:
298
319
  ```plain
299
320
  ***functional specs***
300
321
 
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:.
322
+ # upload-document
323
+ A :User: can upload a :Document:.
324
+ - :DocumentStore: stores the :Document: and assigns it a :DocumentId:.
325
+ - A :User: can retrieve a :Document: by its :DocumentId:.
304
326
  ```
305
327
 
306
328
  ## Meta-commentary and scope disclaimers
@@ -310,13 +332,17 @@ BAD:
310
332
  ```plain
311
333
  ***functional specs***
312
334
 
313
- - This spec is intentionally kept small so that it stays within the complexity limit.
335
+ # export-csv-size-note
336
+ This spec is intentionally kept small so that it stays within the complexity limit.
314
337
 
315
- - As described above, :ExportService: writes a :CsvFile:.
338
+ # export-csv-reference
339
+ As described above, :ExportService: writes a :CsvFile:.
316
340
 
317
- - No changes are made to the existing :ImportService: or to the user interface.
341
+ # export-csv-no-changes
342
+ No changes are made to the existing :ImportService: or to the user interface.
318
343
 
319
- - Future specs will extend this behavior with additional formats.
344
+ # export-csv-future
345
+ Future specs will extend this behavior with additional formats.
320
346
  ```
321
347
 
322
348
  GOOD:
@@ -324,9 +350,10 @@ GOOD:
324
350
  ```plain
325
351
  ***functional specs***
326
352
 
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.
353
+ # export-csv
354
+ :ExportService: writes the :Order: items of a :BillingPeriod: to a :CsvFile:.
355
+ - The header row is `id,customer,total,currency`.
356
+ - An empty :BillingPeriod: produces a :CsvFile: with only the header row.
330
357
  ```
331
358
 
332
359
  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.
@@ -338,13 +365,14 @@ BAD:
338
365
  ```plain
339
366
  ***functional specs***
340
367
 
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% |
368
+ # apply-discounts
369
+ :PricingEngine: applies discounts.
370
+ The discount matrix is as follows:
371
+ | Tier | Volume | Discount |
372
+ |---|---|---|
373
+ | Bronze | 0-99 | 0% |
374
+ | Silver | 100-499 | 5% |
375
+ | Gold | 500+ | 12% |
348
376
  ```
349
377
 
350
378
  GOOD:
@@ -352,8 +380,9 @@ GOOD:
352
380
  ```plain
353
381
  ***functional specs***
354
382
 
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.
383
+ # apply-discounts
384
+ :PricingEngine: applies the :DiscountTier: matrix in [resources/discount-tiers.json](resources/discount-tiers.json) to an :Order: total.
385
+ - An :Order: below the lowest tier threshold gets no discount.
357
386
  ```
358
387
 
359
388
  A section holds `- ` list items. Matrices, schemas, and payloads are external artifacts and are linked from `resources/` — see `linked-resources.md`.
@@ -373,7 +402,8 @@ BAD — everything in one place:
373
402
 
374
403
  ***functional specs***
375
404
 
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.
405
+ # resize-image
406
+ :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
407
  ```
378
408
 
379
409
  GOOD — each fact in the section that owns it:
@@ -395,9 +425,10 @@ GOOD — each fact in the section that owns it:
395
425
 
396
426
  ***functional specs***
397
427
 
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:.
428
+ # resize-image
429
+ :ImageResizer: produces a :Thumbnail: from an :Image:.
430
+ - An :Image: already within the :Thumbnail: bounds is returned unchanged.
431
+ - An :Image: that is not a supported format raises :UnsupportedImageError:.
401
432
  ```
402
433
 
403
434
  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.
@@ -406,26 +437,26 @@ Use `resolve-section-ownership` to perform the split. It is not the same as spli
406
437
 
407
438
  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
439
 
409
- ## Narrated acceptance tests
440
+ ## Narrated scenarios
410
441
 
411
442
  BAD:
412
443
 
413
444
  ```plain
414
- ***acceptance tests***
445
+ ***conformance tests***
415
446
 
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.
447
+ # create-project
448
+ 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
449
  ```
418
450
 
419
451
  GOOD:
420
452
 
421
453
  ```plain
422
- ***acceptance tests***
454
+ ***conformance tests***
423
455
 
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`.
456
+ # create-project
457
+ A signed-in :User: with no :Project: items creates a :Project: named `Apollo` and finds it in :ProjectList:.
458
+ - :ProjectList: contains exactly one :Project:, named `Apollo`.
428
459
  ```
429
460
 
430
- One narrated paragraph becomes an ordered set of checkable steps, and "everything worked as
461
+ One narrated paragraph becomes one checkable sentence, and "everything worked as
431
462
  expected" becomes an assertion.
@@ -31,11 +31,14 @@ BAD:
31
31
 
32
32
  ***functional specs***
33
33
 
34
- - :ProviderClient: retries the request when :ProviderApi: returns 408, 425, 429, 500, 502, 503, or 504, and also on connection errors, DNS failures, socket timeouts, TLS handshake failures, and connection resets.
34
+ # retry-transient-errors
35
+ :ProviderClient: retries the request when :ProviderApi: returns 408, 425, 429, 500, 502, 503, or 504, and also on connection errors, DNS failures, socket timeouts, TLS handshake failures, and connection resets.
35
36
 
36
- - :ProviderClient: does not retry on 400, 401, 403, 404, 405, 409, 410, 415, or 422, and raises immediately.
37
+ # no-retry-permanent-errors
38
+ :ProviderClient: does not retry on 400, 401, 403, 404, 405, 409, 410, 415, or 422, and raises immediately.
37
39
 
38
- - On 429, :ProviderClient: waits and tries again, unless the retry budget is exhausted.
40
+ # retry-on-429
41
+ On 429, :ProviderClient: waits and tries again, unless the retry budget is exhausted.
39
42
  ```
40
43
 
41
44
  GOOD:
@@ -91,13 +94,17 @@ BAD:
91
94
 
92
95
  ***functional specs***
93
96
 
94
- - :ProviderApi: returns `200` with a JSON body when a request succeeds.
97
+ # provider-success-response
98
+ :ProviderApi: returns `200` with a JSON body when a request succeeds.
95
99
 
96
- - :ProviderApi: returns `404` when the requested :Customer: does not exist.
100
+ # provider-not-found
101
+ :ProviderApi: returns `404` when the requested :Customer: does not exist.
97
102
 
98
- - :ProviderApi: rate-limits clients and includes the remaining quota in the response headers.
103
+ # provider-rate-limit
104
+ :ProviderApi: rate-limits clients and includes the remaining quota in the response headers.
99
105
 
100
- - :ProviderApi: requires the `Authorization` header on every request.
106
+ # provider-auth-header
107
+ :ProviderApi: requires the `Authorization` header on every request.
101
108
  ```
102
109
 
103
110
  GOOD:
@@ -123,7 +130,8 @@ GOOD:
123
130
 
124
131
  ***functional specs***
125
132
 
126
- - :ProviderClient: fetches a :Customer: from :ProviderApi: by :CustomerId:.
133
+ # fetch-customer
134
+ :ProviderClient: fetches a :Customer: from :ProviderApi: by :CustomerId:.
127
135
  ```
128
136
 
129
137
  Every BAD bullet is true, and none of them is deleted — they are moved. The success shape, the `404`, and the `Authorization` requirement are what an OpenAPI file exists to state; the rate-limit model has its own conventional files. Provider behavior with no machine-readable home goes in `resources/<provider>-notes.md`. `integrations.md` carries the full artifact table.
@@ -147,19 +155,23 @@ BAD — four specs, one operation:
147
155
 
148
156
  ***functional specs***
149
157
 
150
- - A call to `fetch` fails before any request to :ProviderApi: is sent when `PROVIDER_API_KEY` is unset or empty.
151
- - The failure names `PROVIDER_API_KEY`.
158
+ # fetch-requires-api-key
159
+ A call to `fetch` fails before any request to :ProviderApi: is sent when `PROVIDER_API_KEY` is unset or empty.
160
+ - The failure names `PROVIDER_API_KEY`.
152
161
 
153
- - A call to `fetch` reads the field catalogue from :ProviderApi: once, before requesting any :Record:.
154
- - The catalogue yields the name of every field the account defines.
155
- - A call to `fetch` whose catalogue read fails requests no :Record: items.
162
+ # fetch-reads-catalogue
163
+ A call to `fetch` reads the field catalogue from :ProviderApi: once, before requesting any :Record:.
164
+ - The catalogue yields the name of every field the account defines.
165
+ - A call to `fetch` whose catalogue read fails requests no :Record: items.
156
166
 
157
- - A request for :Record: items from :ProviderApi: names every field in the catalogue and asks for at most 100 records.
158
- - Archived :Record: items are not requested.
167
+ # fetch-requests-records
168
+ A request for :Record: items from :ProviderApi: names every field in the catalogue and asks for at most 100 records.
169
+ - Archived :Record: items are not requested.
159
170
 
160
- - A call to `fetch` requests pages until :ProviderApi: returns a response carrying no next-page cursor.
161
- - Each page after the first is requested with the cursor from the previous response.
162
- - The :Record: items of every page are carried forward together.
171
+ # fetch-pages-records
172
+ A call to `fetch` requests pages until :ProviderApi: returns a response carrying no next-page cursor.
173
+ - Each page after the first is requested with the cursor from the previous response.
174
+ - The :Record: items of every page are carried forward together.
163
175
  ```
164
176
 
165
177
  GOOD — one spec, with the result stated first:
@@ -175,10 +187,11 @@ GOOD — one spec, with the result stated first:
175
187
 
176
188
  ***functional specs***
177
189
 
178
- - :RecordSource: `fetch()` returns every non-archived :Record: with every field :ProviderApi: defines, paging under [resources/pagination.yaml](resources/pagination.yaml).
179
- - :RecordSource: reads the field catalogue once per `fetch()`.
180
- - `fetch()` fails before any request when `PROVIDER_API_KEY` is unset or empty, naming it.
181
- - A failed catalogue read fails `fetch()` before any :Record: is requested.
190
+ # fetch-records
191
+ :RecordSource: `fetch()` returns every non-archived :Record: with every field :ProviderApi: defines, paging under [resources/pagination.yaml](resources/pagination.yaml).
192
+ - :RecordSource: reads the field catalogue once per `fetch()`.
193
+ - `fetch()` fails before any request when `PROVIDER_API_KEY` is unset or empty, naming it.
194
+ - A failed catalogue read fails `fetch()` before any :Record: is requested.
182
195
  ```
183
196
 
184
197
  Merging is not in tension with the complexity limit. Split a spec when it implies more than 200 changed lines of code (`analyze-if-func-spec-too-complex`, then `break-down-func-spec`); merge when several specs implement one operation that fits inside that budget. Fragment count is not a measure of size.
@@ -190,9 +203,11 @@ BAD:
190
203
  ```plain
191
204
  ***functional specs***
192
205
 
193
- - :ProviderAuth: first reads the client id and the client secret, then builds a form-encoded body containing `grant_type=client_credentials`, the client id, and the client secret, then posts that body to the token endpoint, then parses the JSON response, then extracts the `access_token` and the `expires_in` fields, then stores the token in memory together with the moment it expires, and then attaches it to subsequent requests as a bearer token.
206
+ # obtain-token
207
+ :ProviderAuth: first reads the client id and the client secret, then builds a form-encoded body containing `grant_type=client_credentials`, the client id, and the client secret, then posts that body to the token endpoint, then parses the JSON response, then extracts the `access_token` and the `expires_in` fields, then stores the token in memory together with the moment it expires, and then attaches it to subsequent requests as a bearer token.
194
208
 
195
- - Before each request, :ProviderAuth: compares the current time to the stored expiry and, if the token has expired or is about to expire, repeats the whole procedure described above to obtain a new token.
209
+ # refresh-token
210
+ Before each request, :ProviderAuth: compares the current time to the stored expiry and, if the token has expired or is about to expire, repeats the whole procedure described above to obtain a new token.
196
211
  ```
197
212
 
198
213
  GOOD:
@@ -4,7 +4,7 @@ description: Conciseness and plain-language rules that keep .plain specs reviewa
4
4
 
5
5
  # Rules for keeping `.plain` specs concise
6
6
 
7
- These rules apply to every section, to concept explanations, and to acceptance tests. `concise-specs-examples.md` holds a worked BAD to GOOD pair for each failure mode named here, and `concise-specs-integration-examples.md` covers the failure modes specific to integration specs.
7
+ These rules apply to every section, to concept explanations, and to scenarios in test plain files. `concise-specs-examples.md` holds a worked BAD to GOOD pair for each failure mode named here, and `concise-specs-integration-examples.md` covers the failure modes specific to integration specs.
8
8
 
9
9
  ## Why this matters
10
10
 
@@ -53,9 +53,10 @@ meta-commentary, and duplication of an `import` — are in `concise-specs-exampl
53
53
 
54
54
  ## Keep the shape small
55
55
 
56
+ - The bullet rules here apply to `***definitions***`, `***implementation reqs***`, and `***test reqs***`; the body of a functionality and a scenario are free text
56
57
  - One statement per bullet, one bullet per fact (see `bullet-continuation.md`)
57
58
  - Nest a sub-bullet only to remove a real ambiguity; a sub-bullet that merely elaborates is filler
58
- - Consecutive specs that share a subject are usually one spec: if a top-level bullet only tells part of the story of one operation, merge the set, lead with the operation's result, and demote the steps and failure modes to sub-bullets (see *One operation fragmented into several specs* in `concise-specs-integration-examples.md`)
59
+ - Consecutive specs that share a subject are usually one spec: if a functionality only tells part of the story of one operation, merge the set, lead with the operation's result, and demote the steps and failure modes to detail under it (see *One operation fragmented into several specs* in `concise-specs-integration-examples.md`)
59
60
  - Tables, prose paragraphs, and code blocks do not belong in a section; external artifacts are linked from `resources/` (see `linked-resources.md`)
60
61
  - A bullet that answers more than one of *what is it*, *what does it do*, *how is it built*, and *how is it tested* is not one fact — split it across the sections that own those facts, which reads better and is the only way the renderer sees each part (see `module-structure.md`, and *Split one crowded bullet across its owning sections* in `concise-specs-examples.md`)
61
62
 
@@ -1,11 +1,17 @@
1
1
  ---
2
- description: Rules for writing ***functional specs*** and ***acceptance tests*** in .plain files
2
+ description: Rules for writing ***functional specs*** in .plain files
3
3
  ---
4
4
 
5
5
  # Rules for writing `***functional specs***`
6
6
 
7
7
  When writing or editing a `***functional specs***` section in a `.plain` file, always follow these rules:
8
8
 
9
+ ## Shape
10
+ - Each functional spec, called a functionality, is a `# <func-id>` heading followed by its body; the func-id follows `semantic-ids.md`
11
+ - The body is free text that describes the behavior: prose, `- ` bullets, or any other shape the developer prefers; its first line states the behavior
12
+ - No body line starts with `# ` (that starts the next functionality) or with `***`
13
+ - The test scenarios of a functionality live in its test plain file, never in this section (see `test-plain-files.md`)
14
+
9
15
  ## Voice
10
16
  - 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
17
  - **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`)
@@ -18,11 +24,14 @@ BAD — imperative commands, or the vague "The system":
18
24
  ```plain
19
25
  ***functional specs***
20
26
 
21
- - Implement the entry point for :App:.
27
+ # entry-point
28
+ Implement the entry point for :App:.
22
29
 
23
- - The system shows :TaskList:.
30
+ # show-task-list
31
+ The system shows :TaskList:.
24
32
 
25
- - Add :Task:.
33
+ # add-task
34
+ Add :Task:.
26
35
  ```
27
36
 
28
37
  GOOD — statements of fact with concrete subjects:
@@ -30,11 +39,14 @@ GOOD — statements of fact with concrete subjects:
30
39
  ```plain
31
40
  ***functional specs***
32
41
 
33
- - :App: has an entry point.
42
+ # entry-point
43
+ :App: has an entry point.
34
44
 
35
- - The :TaskList: is displayed.
45
+ # show-task-list
46
+ The :TaskList: is displayed.
36
47
 
37
- - A :User: can add a :Task:. Only valid :Task: items are added.
48
+ # add-task
49
+ A :User: can add a :Task:. Only valid :Task: items are added.
38
50
  ```
39
51
 
40
52
  ## Conciseness
@@ -80,17 +92,17 @@ GOOD — statements of fact with concrete subjects:
80
92
 
81
93
  ## Disambiguation
82
94
  - Each functional spec must be unambiguous — the renderer should have only one reasonable interpretation
83
- - If a single line is not enough to fully disambiguate the behavior, use **nested sub-bullets** to add detail
84
- - Nested lines clarify the parent spec — they do not introduce separate functionality
85
- - Even with nested detail, the spec must still imply ≤ 200 lines of code
95
+ - If the statement line is not enough to fully disambiguate the behavior, add `- ` bullets or sentences under it to add detail
96
+ - The detail clarifies the statement — it does not introduce separate functionality
97
+ - Even with the detail, the spec must still imply ≤ 200 lines of code
86
98
 
87
99
  ## Testability
88
100
  - Every functional spec must describe behavior that can be verified through an observable interface
89
101
  - State the triggering input or action and the deterministic outcome a test can observe
90
102
  - Include relevant outputs, state changes, errors, or externally visible side effects
91
103
  - Do not rely on generated implementation details as the only way to verify the behavior
92
- - If a black-box conformance test cannot be derived from the spec, make the behavior more explicit
93
- - Testability does not require an acceptance test; acceptance tests are reserved for full workflows
104
+ - If a black-box conformance scenario cannot be written from the spec, make the behavior more explicit
105
+ - When conformance tests are enabled, every functionality has a test plain file with at least one conformance scenario (see `test-plain-files.md`); write it in the same step as the functional spec
94
106
 
95
107
  ## Deterministic interface
96
108
  - Specs must be detailed enough that a developer can use the built software without reading the generated code
@@ -102,28 +114,25 @@ GOOD — statements of fact with concrete subjects:
102
114
  - `requires` modules only pass on functional specs and `exported_concepts` — never implementation reqs, so do not rely on implementation reqs to convey behavior
103
115
  - Behavior that downstream modules need must be expressed in functional specs, not elsewhere
104
116
 
105
- ## Acceptance tests
106
- - Nest `***acceptance tests***` under a functional spec when verification criteria are needed
107
- - 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
108
- - Do not restate the obvious behavior from the functional spec — simple, direct verifications are already auto-generated as conformance tests. Acceptance tests must go beyond that: multi-step workflows, interactions between concepts, edge-case scenarios, or end-to-end sequences that prove the feature works in a realistic context
109
- - Each acceptance test must be a direct logical consequence of the parent spec — it illustrates, not extends
110
- - Acceptance tests must describe concrete, verifiable outcomes — not vague qualities
111
- - Acceptance tests must not contradict, narrow, or extend beyond the parent spec
117
+ ## Test scenarios live in test plain files
118
+ - A spec plain file never holds a `***acceptance tests***` section, nested or top-level
119
+ - Conformance and acceptance scenarios for a functionality go into its test plain file, by convention `tests/<module>/<func-id>.plain`, written with `add-test-scenarios` (see `test-plain-files.md`)
120
+ - A functional spec and its test plain file change together: a spec edit without a scenario review is not finished
112
121
 
113
122
  ## Format
114
123
 
115
124
  ```plain
116
125
  ***functional specs***
117
126
 
118
- - :App: has an entry point.
119
-
120
- - A :User: can add a :Task:. Only valid :Task: items are added.
127
+ # entry-point
128
+ :App: has an entry point.
121
129
 
122
- - A :User: can send a :Message: to a :Conversation:.
123
- - A :Message: must have non-empty content.
124
- - The :Message: is appended to the end of the :Conversation:.
125
- - All :Participant: members of the :Conversation: can see the new :Message:.
130
+ # add-task
131
+ A :User: can add a :Task:. Only valid :Task: items are added.
126
132
 
127
- ***acceptance tests***
128
- - Sending a :Message: to a :Conversation: with three participants should make the message visible to all three.
133
+ # send-message
134
+ A :User: can send a :Message: to a :Conversation:.
135
+ - A :Message: must have non-empty content.
136
+ - The :Message: is appended to the end of the :Conversation:.
137
+ - All :Participant: members of the :Conversation: can see the new :Message:.
129
138
  ```
@@ -32,7 +32,7 @@ When writing or editing an `***implementation reqs***` section in a `.plain` fil
32
32
  - Behavior and features → `***functional specs***`
33
33
  - Concept definitions → `***definitions***`
34
34
  - **Conformance-test guidance** → `***test reqs***`
35
- - **Acceptance-test scenarios** → `***acceptance tests***` nested under the relevant functional spec
35
+ - **Test scenarios**, conformance and acceptance → the functionality's test plain file (see `test-plain-files.md`)
36
36
 
37
37
  ## Unit tests vs conformance tests (common mistake)
38
38
  - Unit-test guidance (`:UnitTests:` framework, structure, mocking) goes **here**, not in `***test reqs***`
@@ -286,4 +286,4 @@ Use them as a template when adding a new embedded integration in Java / Maven
286
286
  4. Wire them into the module's `config.yaml` via the `unittests-script`, `conformance-tests-script`, and `prepare-environment-script` keys. Use `init-config-file` to assemble the canonical form
287
287
  5. Smoke-test by running each script manually with the renderer's output folder as `$1` (and the conformance-tests folder as `$2` for the conformance runner). Confirm exit codes match what the renderer expects
288
288
 
289
- For non-Java languages, the plain-forge skills [`implement-prepare-environment-script`](../skills/implement-prepare-environment-script/SKILL.md), [`implement-unit-testing-script`](../skills/implement-unit-testing-script/SKILL.md), and [`implement-conformance-testing-script`](../skills/implement-conformance-testing-script/SKILL.md) adapt the contract above to Python, Node.js, Go, Rust, Flutter, and others. Always route script creation through these skills so the cross-cutting rules above are applied uniformly.
289
+ For non-Java languages, the plain-forge skills [`implement-prepare-environment-script`](../../implement-prepare-environment-script/SKILL.md), [`implement-unit-testing-script`](../../implement-unit-testing-script/SKILL.md), and [`implement-conformance-testing-script`](../../implement-conformance-testing-script/SKILL.md) adapt the contract above to Python, Node.js, Go, Rust, Flutter, and others. Always route script creation through these skills so the cross-cutting rules above are applied uniformly.