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.
- package/README.md +14 -11
- package/bin/cli.mjs +5 -4
- package/forge/rules/plain-forge.md +10 -0
- package/forge/skills/add-concept/SKILL.md +1 -1
- package/forge/skills/add-feature/SKILL.md +8 -7
- package/forge/skills/add-feature/references/checklist.md +2 -2
- package/forge/skills/add-functional-spec/SKILL.md +14 -10
- package/forge/skills/add-functional-specs/SKILL.md +14 -10
- package/forge/skills/add-implementation-requirement/SKILL.md +1 -1
- package/forge/skills/add-resource/SKILL.md +3 -2
- package/forge/skills/add-template/SKILL.md +1 -1
- package/forge/skills/add-test-requirement/SKILL.md +4 -4
- package/forge/skills/add-test-scenarios/SKILL.md +98 -0
- package/forge/skills/analyze-2-func-specs/SKILL.md +1 -1
- package/forge/skills/analyze-func-specs/SKILL.md +2 -2
- package/forge/skills/analyze-if-func-spec-too-complex/SKILL.md +44 -24
- package/forge/skills/break-down-func-spec/SKILL.md +57 -33
- package/forge/skills/check-plain-env/SKILL.md +2 -2
- package/forge/skills/consolidate-concepts/SKILL.md +8 -8
- package/forge/skills/create-import-module/SKILL.md +1 -1
- package/forge/skills/create-requires-module/SKILL.md +6 -3
- package/forge/skills/debug-specs/SKILL.md +14 -8
- package/forge/skills/forge-plain/SKILL.md +3 -3
- package/forge/skills/forge-plain/references/checklist.md +3 -3
- package/forge/skills/forge-plain/references/phase-1-product.md +2 -2
- package/forge/skills/forge-plain/references/phase-2-tech.md +1 -1
- package/forge/skills/forge-plain/references/phase-3-testing.md +5 -5
- package/forge/skills/init-config-file/SKILL.md +2 -2
- package/forge/skills/init-plain-project/SKILL.md +6 -6
- package/forge/skills/load-plain-reference/SKILL.md +25 -25
- package/forge/skills/load-plain-reference/references/project-model.md +2 -1
- package/forge/skills/load-plain-reference/references/rendering-and-testing.md +7 -5
- package/forge/{rules → skills/load-plain-reference/rules}/bullet-continuation.md +11 -11
- package/forge/{rules → skills/load-plain-reference/rules}/concise-specs-examples.md +100 -69
- package/forge/{rules → skills/load-plain-reference/rules}/concise-specs-integration-examples.md +39 -24
- package/forge/{rules → skills/load-plain-reference/rules}/concise-specs.md +3 -2
- package/forge/{rules → skills/load-plain-reference/rules}/func-specs.md +37 -28
- package/forge/{rules → skills/load-plain-reference/rules}/impl-reqs.md +1 -1
- package/forge/{rules → skills/load-plain-reference/rules}/integration-embedded-testing.md +1 -1
- package/forge/{rules → skills/load-plain-reference/rules}/linked-resources.md +7 -4
- package/forge/{rules → skills/load-plain-reference/rules}/module-structure.md +16 -9
- package/forge/{rules → skills/load-plain-reference/rules}/requires-modules.md +1 -0
- package/forge/skills/load-plain-reference/rules/semantic-ids.md +47 -0
- package/forge/skills/load-plain-reference/rules/test-plain-files.md +67 -0
- package/forge/{rules → skills/load-plain-reference/rules}/test-reqs.md +6 -6
- package/forge/skills/plain-healthcheck/SKILL.md +31 -13
- package/forge/skills/refactor-module/SKILL.md +16 -16
- package/forge/skills/resolve-section-ownership/SKILL.md +3 -3
- package/forge/skills/resolve-spec-conflict/SKILL.md +14 -8
- package/package.json +1 -1
- package/forge/skills/add-acceptance-test/SKILL.md +0 -101
- /package/forge/{rules → skills/load-plain-reference/rules}/definitions.md +0 -0
- /package/forge/{rules → skills/load-plain-reference/rules}/examples/integration-embedded-testing/prepare_environment_java.sh +0 -0
- /package/forge/{rules → skills/load-plain-reference/rules}/examples/integration-embedded-testing/run_conformance_tests_java.sh +0 -0
- /package/forge/{rules → skills/load-plain-reference/rules}/examples/integration-embedded-testing/run_unittests_java.sh +0 -0
- /package/forge/{rules → skills/load-plain-reference/rules}/exported-concepts.md +0 -0
- /package/forge/{rules → skills/load-plain-reference/rules}/import-modules.md +0 -0
- /package/forge/{rules → skills/load-plain-reference/rules}/integration-embedded.md +0 -0
- /package/forge/{rules → skills/load-plain-reference/rules}/integration-standalone.md +0 -0
- /package/forge/{rules → skills/load-plain-reference/rules}/integrations.md +0 -0
- /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
|
-
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
-
|
|
102
|
-
|
|
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
|
-
-
|
|
124
|
+
# validate-input
|
|
125
|
+
:OrderService: validates all incoming data before processing it.
|
|
117
126
|
|
|
118
|
-
-
|
|
127
|
+
# handle-errors
|
|
128
|
+
:OrderService: handles errors and returns meaningful error messages.
|
|
119
129
|
|
|
120
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
-
|
|
277
|
-
|
|
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
|
-
|
|
305
|
+
# upload-document
|
|
306
|
+
A :User: can upload a :Document:.
|
|
288
307
|
|
|
289
|
-
-
|
|
308
|
+
# store-document
|
|
309
|
+
The system stores the submitted file in :DocumentStore:.
|
|
290
310
|
|
|
291
|
-
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
-
|
|
335
|
+
# export-csv-size-note
|
|
336
|
+
This spec is intentionally kept small so that it stays within the complexity limit.
|
|
314
337
|
|
|
315
|
-
-
|
|
338
|
+
# export-csv-reference
|
|
339
|
+
As described above, :ExportService: writes a :CsvFile:.
|
|
316
340
|
|
|
317
|
-
-
|
|
341
|
+
# export-csv-no-changes
|
|
342
|
+
No changes are made to the existing :ImportService: or to the user interface.
|
|
318
343
|
|
|
319
|
-
-
|
|
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
|
-
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
440
|
+
## Narrated scenarios
|
|
410
441
|
|
|
411
442
|
BAD:
|
|
412
443
|
|
|
413
444
|
```plain
|
|
414
|
-
***
|
|
445
|
+
***conformance tests***
|
|
415
446
|
|
|
416
|
-
|
|
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
|
-
***
|
|
454
|
+
***conformance tests***
|
|
423
455
|
|
|
424
|
-
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
461
|
+
One narrated paragraph becomes one checkable sentence, and "everything worked as
|
|
431
462
|
expected" becomes an assertion.
|
package/forge/{rules → skills/load-plain-reference/rules}/concise-specs-integration-examples.md
RENAMED
|
@@ -31,11 +31,14 @@ BAD:
|
|
|
31
31
|
|
|
32
32
|
***functional specs***
|
|
33
33
|
|
|
34
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
97
|
+
# provider-success-response
|
|
98
|
+
:ProviderApi: returns `200` with a JSON body when a request succeeds.
|
|
95
99
|
|
|
96
|
-
-
|
|
100
|
+
# provider-not-found
|
|
101
|
+
:ProviderApi: returns `404` when the requested :Customer: does not exist.
|
|
97
102
|
|
|
98
|
-
-
|
|
103
|
+
# provider-rate-limit
|
|
104
|
+
:ProviderApi: rate-limits clients and includes the remaining quota in the response headers.
|
|
99
105
|
|
|
100
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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***
|
|
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
|
-
|
|
27
|
+
# entry-point
|
|
28
|
+
Implement the entry point for :App:.
|
|
22
29
|
|
|
23
|
-
-
|
|
30
|
+
# show-task-list
|
|
31
|
+
The system shows :TaskList:.
|
|
24
32
|
|
|
25
|
-
-
|
|
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
|
-
|
|
42
|
+
# entry-point
|
|
43
|
+
:App: has an entry point.
|
|
34
44
|
|
|
35
|
-
-
|
|
45
|
+
# show-task-list
|
|
46
|
+
The :TaskList: is displayed.
|
|
36
47
|
|
|
37
|
-
|
|
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
|
|
84
|
-
-
|
|
85
|
-
- Even with
|
|
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
|
|
93
|
-
-
|
|
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
|
-
##
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
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
|
-
|
|
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
|
-
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
- **
|
|
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`](
|
|
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.
|