plain-forge 1.0.19 → 1.0.21-next.4f1def5
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 +3 -2
- package/bin/cli.mjs +1 -1
- package/forge/rules/bullet-continuation.md +16 -6
- package/forge/rules/concise-specs-examples.md +431 -0
- package/forge/rules/concise-specs-integration-examples.md +272 -0
- package/forge/rules/concise-specs.md +80 -0
- package/forge/rules/definitions.md +7 -4
- package/forge/rules/exported-concepts.md +3 -2
- package/forge/rules/func-specs.md +10 -3
- package/forge/rules/impl-reqs.md +9 -5
- package/forge/rules/import-modules.md +4 -6
- package/forge/rules/integration-embedded-testing.md +3 -3
- package/forge/rules/integration-embedded.md +4 -2
- package/forge/rules/integration-standalone.md +1 -1
- package/forge/rules/integrations.md +6 -7
- package/forge/rules/linked-resources.md +9 -8
- package/forge/rules/module-structure.md +105 -0
- package/forge/rules/required-concepts.md +5 -5
- package/forge/rules/requires-modules.md +37 -21
- package/forge/rules/test-reqs.md +12 -8
- package/forge/skills/add-acceptance-test/SKILL.md +1 -0
- package/forge/skills/add-concept/SKILL.md +2 -1
- package/forge/skills/add-feature/SKILL.md +2 -2
- package/forge/skills/add-functional-spec/SKILL.md +2 -1
- package/forge/skills/add-functional-specs/SKILL.md +2 -1
- package/forge/skills/add-implementation-requirement/SKILL.md +2 -1
- package/forge/skills/add-test-requirement/SKILL.md +1 -0
- package/forge/skills/break-down-func-spec/SKILL.md +1 -0
- package/forge/skills/check-forge-version/SKILL.md +154 -0
- package/forge/skills/check-forge-version/scripts/check-forge-version.mjs +368 -0
- package/forge/skills/create-import-module/SKILL.md +3 -3
- package/forge/skills/create-requires-module/SKILL.md +25 -20
- package/forge/skills/debug-specs/SKILL.md +5 -5
- package/forge/skills/forge-plain/SKILL.md +2 -2
- package/forge/skills/forge-plain/references/checklist.md +2 -1
- package/forge/skills/forge-plain/references/phase-1-product.md +3 -2
- package/forge/skills/forge-plain/references/phase-2-tech.md +1 -1
- package/forge/skills/implement-conformance-testing-script/SKILL.md +2 -2
- package/forge/skills/implement-prepare-environment-script/SKILL.md +2 -2
- package/forge/skills/implement-unit-testing-script/SKILL.md +1 -1
- package/forge/skills/init-config-file/SKILL.md +2 -3
- package/forge/skills/load-codeplain-reference/SKILL.md +4 -4
- package/forge/skills/load-plain-reference/SKILL.md +20 -2
- package/forge/skills/load-plain-reference/references/project-model.md +3 -13
- package/forge/skills/load-plain-reference/references/rendering-and-testing.md +5 -5
- package/forge/skills/plain-healthcheck/SKILL.md +2 -2
- package/forge/skills/refactor-module/SKILL.md +3 -3
- package/forge/skills/resolve-section-ownership/SKILL.md +65 -0
- package/forge/skills/resolve-spec-conflict/SKILL.md +1 -0
- package/forge/skills/run-codeplain/SKILL.md +14 -14
- package/package.json +5 -5
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Worked BAD to GOOD examples of concise integration specs, where over-precision and obvious edge cases dominate
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Worked examples of concise integration specs
|
|
6
|
+
|
|
7
|
+
`concise-specs.md` is the rule and `concise-specs-examples.md` is the general example bank. This file covers integration specs only, because they fail differently.
|
|
8
|
+
|
|
9
|
+
An integration is technically demanding, the provider's contract is large, and the author has the provider's documentation open while writing. The result is a spec that is *too precise*: status codes, field lists, retry arithmetic, and every edge case the API can produce, written out as prose bullets. It reads as rigorous and is unreviewable.
|
|
10
|
+
|
|
11
|
+
Conciseness here is almost never about covering less. `integrations.md` makes edge-case coverage a hard floor — auth, environments, versioning, pagination, rate limits, the error model, retries, idempotency, webhooks, data mapping, compliance, observability. Every one of those must be covered. The lever is **where** the coverage lives:
|
|
12
|
+
|
|
13
|
+
- **Shapes and enumerations go to `resources/`** — schemas, error maps, retry policy, rate-limit headers. A spec bullet links to the file; it never restates its contents.
|
|
14
|
+
- **The spec keeps behavior** — what the integration does with the shape, and what a caller observes.
|
|
15
|
+
- **Mechanism goes to `***implementation reqs***`** — retry logic, exception hierarchies, token caching, connection handling. Integration precision is mostly HOW, and `impl-reqs.md` owns HOW. `***functional specs***` keeps only what a caller can observe.
|
|
16
|
+
|
|
17
|
+
That last lever is easy to miss, so several examples below move a bullet between sections rather than shortening it. A fact in the wrong section is worse than a verbose one: the renderer reads each concept only from its owning section, so a misplaced fact is silently ignored.
|
|
18
|
+
|
|
19
|
+
Applied consistently, a spec gets shorter as coverage goes *up*, because each new corner case adds a line to a resource file rather than a paragraph to the spec.
|
|
20
|
+
|
|
21
|
+
The BAD blocks intentionally demonstrate the mistakes they name, including invalid syntax. Copy only from GOOD blocks.
|
|
22
|
+
|
|
23
|
+
## Status codes and error categories enumerated inline
|
|
24
|
+
|
|
25
|
+
BAD:
|
|
26
|
+
|
|
27
|
+
```plain
|
|
28
|
+
***definitions***
|
|
29
|
+
|
|
30
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
31
|
+
|
|
32
|
+
***functional specs***
|
|
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.
|
|
35
|
+
|
|
36
|
+
- :ProviderClient: does not retry on 400, 401, 403, 404, 405, 409, 410, 415, or 422, and raises immediately.
|
|
37
|
+
|
|
38
|
+
- On 429, :ProviderClient: waits and tries again, unless the retry budget is exhausted.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
GOOD:
|
|
42
|
+
|
|
43
|
+
```plain
|
|
44
|
+
***definitions***
|
|
45
|
+
|
|
46
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
47
|
+
|
|
48
|
+
***implementation reqs***
|
|
49
|
+
|
|
50
|
+
- :ProviderClient: classifies every failure of :ProviderApi: through [resources/error-map.yaml](resources/error-map.yaml) and retries the transient ones under [resources/retry-policy.yaml](resources/retry-policy.yaml).
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The status-code lists move into `error-map.yaml`, which the renderer already reads to generate the exception hierarchy and the transient predicate. Coverage is unchanged and the classification now has one source of truth instead of two that can disagree — see *Transient vs. permanent exception classification* in `integrations.md`.
|
|
54
|
+
|
|
55
|
+
Note the section too. Classifying and retrying is mechanism, so it is an implementation req; what belongs in `***functional specs***` is only the part a caller observes — which exception each error category surfaces as.
|
|
56
|
+
|
|
57
|
+
## Restating a schema the resource already carries
|
|
58
|
+
|
|
59
|
+
BAD:
|
|
60
|
+
|
|
61
|
+
```plain
|
|
62
|
+
***definitions***
|
|
63
|
+
|
|
64
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
65
|
+
|
|
66
|
+
- :Customer: is a customer record returned by :ProviderApi:. It has an `id` (string), an `email` (string), a `created_at` (ISO 8601 timestamp), a `status` (one of `active`, `pending`, `churned`), an optional `phone` (string or null), and a `metadata` object of string keys to string values.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
GOOD:
|
|
70
|
+
|
|
71
|
+
```plain
|
|
72
|
+
***definitions***
|
|
73
|
+
|
|
74
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
75
|
+
|
|
76
|
+
- :Customer: is a :ProviderApi: record, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml) under `components.schemas.Customer`.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Field lists rot the moment the provider adds a field, and the renderer generates types from the schema, not from the sentence. Keep in the definition only what the schema cannot say — a stability or uniqueness guarantee, a unit, a lifecycle rule.
|
|
80
|
+
|
|
81
|
+
## Specifying the provider instead of the integration
|
|
82
|
+
|
|
83
|
+
BAD:
|
|
84
|
+
|
|
85
|
+
```plain
|
|
86
|
+
***definitions***
|
|
87
|
+
|
|
88
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
89
|
+
|
|
90
|
+
- :Customer: is a :ProviderApi: record, at `components.schemas.Customer`.
|
|
91
|
+
|
|
92
|
+
***functional specs***
|
|
93
|
+
|
|
94
|
+
- :ProviderApi: returns `200` with a JSON body when a request succeeds.
|
|
95
|
+
|
|
96
|
+
- :ProviderApi: returns `404` when the requested :Customer: does not exist.
|
|
97
|
+
|
|
98
|
+
- :ProviderApi: rate-limits clients and includes the remaining quota in the response headers.
|
|
99
|
+
|
|
100
|
+
- :ProviderApi: requires the `Authorization` header on every request.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
GOOD:
|
|
104
|
+
|
|
105
|
+
```plain
|
|
106
|
+
***definitions***
|
|
107
|
+
|
|
108
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
109
|
+
|
|
110
|
+
- :ProviderClient: is the component every call to :ProviderApi: goes through.
|
|
111
|
+
|
|
112
|
+
- :Customer: is a :ProviderApi: record, at `components.schemas.Customer`.
|
|
113
|
+
|
|
114
|
+
- :CustomerId: identifies a :Customer: and is stable across updates.
|
|
115
|
+
|
|
116
|
+
- :CustomerNotFoundError: is raised when :ProviderApi: has no :Customer: for a :CustomerId:.
|
|
117
|
+
|
|
118
|
+
***implementation reqs***
|
|
119
|
+
|
|
120
|
+
- :ProviderClient: authenticates to :ProviderApi: with the credential in the environment variable `PROVIDER_API_KEY`.
|
|
121
|
+
|
|
122
|
+
- :ProviderClient: raises :CustomerNotFoundError: when :ProviderApi: has no :Customer: for the requested :CustomerId:.
|
|
123
|
+
|
|
124
|
+
***functional specs***
|
|
125
|
+
|
|
126
|
+
- :ProviderClient: fetches a :Customer: from :ProviderApi: by :CustomerId:.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
What stays in the sections is only what gets built. A bullet stating provider behavior renders into nothing — the renderer cannot build the provider, and no conformance test can assert a sentence that makes no claim about the integration. State instead what the integration does when the provider behaves that way.
|
|
132
|
+
|
|
133
|
+
Note where the links hang: both artifacts attach to the :ProviderApi: concept, and everything else references the token. `linked-resources.md` requires each resource to be linked from exactly one place, so a second spec needing the contract cannot introduce a second, divergent reference to it.
|
|
134
|
+
|
|
135
|
+
## One operation fragmented into several specs
|
|
136
|
+
|
|
137
|
+
Integration work invites this: the author walks through the request lifecycle — credential check, discovery call, request shape, paging — and each step becomes its own spec.
|
|
138
|
+
|
|
139
|
+
BAD — four specs, one operation:
|
|
140
|
+
|
|
141
|
+
```plain
|
|
142
|
+
***definitions***
|
|
143
|
+
|
|
144
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
145
|
+
|
|
146
|
+
- :Record: is an account record of :ProviderApi:.
|
|
147
|
+
|
|
148
|
+
***functional specs***
|
|
149
|
+
|
|
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`.
|
|
152
|
+
|
|
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.
|
|
156
|
+
|
|
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.
|
|
159
|
+
|
|
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.
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
GOOD — one spec, with the result stated first:
|
|
166
|
+
|
|
167
|
+
```plain
|
|
168
|
+
***definitions***
|
|
169
|
+
|
|
170
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
171
|
+
|
|
172
|
+
- :Record: is an account record of :ProviderApi:.
|
|
173
|
+
|
|
174
|
+
- :RecordSource: is the component through which the integration reads :Record: items from :ProviderApi:.
|
|
175
|
+
|
|
176
|
+
***functional specs***
|
|
177
|
+
|
|
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.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
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.
|
|
185
|
+
|
|
186
|
+
## The auth flow narrated step by step
|
|
187
|
+
|
|
188
|
+
BAD:
|
|
189
|
+
|
|
190
|
+
```plain
|
|
191
|
+
***functional specs***
|
|
192
|
+
|
|
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.
|
|
194
|
+
|
|
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.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
GOOD:
|
|
199
|
+
|
|
200
|
+
```plain
|
|
201
|
+
***definitions***
|
|
202
|
+
|
|
203
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
204
|
+
|
|
205
|
+
- :ProviderClient: is the component every call to :ProviderApi: goes through.
|
|
206
|
+
|
|
207
|
+
- :AccessToken: is the bearer credential :ProviderApi: accepts.
|
|
208
|
+
|
|
209
|
+
- :AuthTokenManager: manages the lifecycle of all :AccessToken:
|
|
210
|
+
|
|
211
|
+
***implementation reqs***
|
|
212
|
+
|
|
213
|
+
- :ProviderAuth: obtains an :AccessToken: by the grant in [resources/provider.openapi.yaml](resources/provider.openapi.yaml) under `components.securitySchemes.oauth2`.
|
|
214
|
+
|
|
215
|
+
- :ProviderAuth: reads `PROVIDER_CLIENT_ID` and `PROVIDER_CLIENT_SECRET` from the environment.
|
|
216
|
+
|
|
217
|
+
- :AuthTokenManager: refreshes an :AccessToken: 60 seconds before it expires, and once on a `401` from :ProviderApi:.
|
|
218
|
+
|
|
219
|
+
- Concurrent requests finding the :AccessToken: expired cause one refresh.
|
|
220
|
+
|
|
221
|
+
- :ProviderClient: uses :AuthTokenManager: to cache and refresh :AccessToken:.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The grant type, endpoint, body encoding, and response fields are in the security scheme. What
|
|
225
|
+
survives is the policy the scheme does not describe: the env-var names, the refresh margin, the
|
|
226
|
+
retry-once-on-401 rule, and the concurrency behavior.
|
|
227
|
+
|
|
228
|
+
Almost all of it is implementation reqs — acquisition, caching, and refresh are machinery a caller never sees. Not all of it, though, and the rewrite has to name a component to say the rest. :ProviderAuth: performs the grant and has no observable surface; :AuthTokenManager: is what consumes it and hands tokens to :ProviderClient:, so it is the concept a functional spec can take as its subject. Without that consumer the section is left with a subject-less bullet, which `func-specs.md` rules out — and that one spec is the only line here a live conformance test can assert.
|
|
229
|
+
|
|
230
|
+
## Per-endpoint test reqs
|
|
231
|
+
|
|
232
|
+
BAD:
|
|
233
|
+
|
|
234
|
+
```plain
|
|
235
|
+
***definitions***
|
|
236
|
+
|
|
237
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
238
|
+
|
|
239
|
+
***test reqs***
|
|
240
|
+
|
|
241
|
+
- :ConformanceTests: for the customer list endpoint run against the live :ProviderApi: sandbox.
|
|
242
|
+
|
|
243
|
+
- :ConformanceTests: for the customer retrieval endpoint run against the live :ProviderApi: sandbox.
|
|
244
|
+
|
|
245
|
+
- :ConformanceTests: for the invoice list endpoint run against the live :ProviderApi: sandbox.
|
|
246
|
+
|
|
247
|
+
- :ConformanceTests: for the webhook endpoint run against the live :ProviderApi: sandbox.
|
|
248
|
+
|
|
249
|
+
- :ConformanceTests: for the customer list endpoint use the credentials from the environment.
|
|
250
|
+
|
|
251
|
+
- :ConformanceTests: for the invoice list endpoint use the credentials from the environment.
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
GOOD:
|
|
255
|
+
|
|
256
|
+
```plain
|
|
257
|
+
***definitions***
|
|
258
|
+
|
|
259
|
+
- :ProviderApi: is the REST surface of the provider, defined by [resources/provider.openapi.yaml](resources/provider.openapi.yaml).
|
|
260
|
+
|
|
261
|
+
***test reqs***
|
|
262
|
+
|
|
263
|
+
- :ConformanceTests: run against the live :ProviderApi: sandbox — no recorded fixtures and no mocking of provider calls.
|
|
264
|
+
- Credentials come from `PROVIDER_CLIENT_ID` and `PROVIDER_CLIENT_SECRET` in the environment.
|
|
265
|
+
- The `429` and forced-`5xx` paths mock that endpoint alone; every other path is live.
|
|
266
|
+
|
|
267
|
+
- :ConformanceTests: are executed via the run script [test_scripts/run_conformance_tests_python.sh](test_scripts/run_conformance_tests_python.sh).
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
A policy that holds for every endpoint is stated once. Enumerate an endpoint only where it departs
|
|
271
|
+
from the policy — here, the two paths that cannot be exercised live safely, which
|
|
272
|
+
`integrations.md` requires to be documented explicitly.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Conciseness and plain-language rules that keep .plain specs reviewable by a human
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Rules for keeping `.plain` specs concise
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Why this matters
|
|
10
|
+
|
|
11
|
+
- A spec is the source of truth, so a human has to be able to review it
|
|
12
|
+
- A long, dense, or ornate spec is skipped rather than reviewed — an unreviewed spec is an unverified spec, however correct it happens to be
|
|
13
|
+
- Length is not a proxy for rigor: a spec earns trust by being checkable, not by being thorough-looking
|
|
14
|
+
- Conciseness is about removing what carries no information, never about removing required detail — the deterministic-interface, disambiguation, and testability rules always win (see `func-specs.md`)
|
|
15
|
+
|
|
16
|
+
## Do not write the obvious
|
|
17
|
+
|
|
18
|
+
Delete any line a competent reader would already assume. Ask of every line: *if this were missing, would anything be built differently?* If not, cut it.
|
|
19
|
+
|
|
20
|
+
Do not write:
|
|
21
|
+
|
|
22
|
+
- Restatements of a concept's own name (`:UserEmail: is the email address of a :User:.` adds nothing beyond what `definitions.md` already requires the concept to carry)
|
|
23
|
+
- Universal software defaults: that invalid input is rejected, that errors are reported, that data is saved when saving is the point of the operation
|
|
24
|
+
- Rationale, motivation, benefits, or background — the spec states what is, not why it was chosen
|
|
25
|
+
- Standard behavior of a technology already named in `***implementation reqs***` (an HTTP framework parses HTTP; a database driver opens connections)
|
|
26
|
+
- Restatements of a nearby bullet in different words, or of anything already stated in an `import`ed or `require`d module
|
|
27
|
+
- Anything about work the spec is not about ("no changes to the existing UI")
|
|
28
|
+
- Meta-commentary about the spec itself ("this spec is intentionally small", "as described above")
|
|
29
|
+
|
|
30
|
+
Worked pairs for every item above — padding, rationale, defaults, technology behavior,
|
|
31
|
+
meta-commentary, and duplication of an `import` — are in `concise-specs-examples.md`.
|
|
32
|
+
|
|
33
|
+
## Use plain words
|
|
34
|
+
|
|
35
|
+
- Prefer the shortest word that is still exact; a technical term needs to be exact, an elaborate one is not
|
|
36
|
+
- Write short sentences: one clause plus, at most, one qualifier
|
|
37
|
+
- Use the same word for the same thing every time — synonym variety reads as two different things
|
|
38
|
+
- Do not use adverbs and adjectives that no test can check: `robust`, `seamless`, `efficient`, `scalable`, `comprehensive`, `modern`, `properly`, `gracefully`, `as appropriate`, `where applicable`, `best practices`. Either state the checkable fact (`a response is returned within 200 ms`) or delete the word
|
|
39
|
+
- Do not hedge: `should ideally`, `may optionally`, `typically`, `generally`, `if possible`. A spec states one behavior; a hedge tells the renderer to guess
|
|
40
|
+
|
|
41
|
+
| Do not write | Write |
|
|
42
|
+
|---|---|
|
|
43
|
+
| utilize, leverage | use |
|
|
44
|
+
| facilitate, enable the ability to | let, or name the action |
|
|
45
|
+
| in order to | to |
|
|
46
|
+
| prior to / subsequent to | before / after |
|
|
47
|
+
| in the event that | if |
|
|
48
|
+
| is responsible for handling | handles |
|
|
49
|
+
| performs the validation of | validates |
|
|
50
|
+
| a plurality of | several, or a number |
|
|
51
|
+
| terminate, initiate | stop, start |
|
|
52
|
+
| functionality for X | X |
|
|
53
|
+
|
|
54
|
+
## Keep the shape small
|
|
55
|
+
|
|
56
|
+
- One statement per bullet, one bullet per fact (see `bullet-continuation.md`)
|
|
57
|
+
- 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
|
+
- Tables, prose paragraphs, and code blocks do not belong in a section; external artifacts are linked from `resources/` (see `linked-resources.md`)
|
|
60
|
+
- 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
|
+
## Terse is not a defect
|
|
63
|
+
|
|
64
|
+
A finished spec usually reads as abrupt: clipped bullets, no connective tissue, no lead-ins. That is the intended shape, not something to smooth out afterwards.
|
|
65
|
+
|
|
66
|
+
- Add a word only to make a line correct, unambiguous, or checkable — never to make it read better
|
|
67
|
+
- Do not merge short bullets into one sentence for rhythm, and do not open a section or a bullet with a lead-in
|
|
68
|
+
- The bar is the other rules — correct, intent intact, reviewable, testable. A spec that clears them is finished however clipped it sounds
|
|
69
|
+
- Filler is not neutral: the renderer reads every line as a requirement, so a line written for flow can invent one
|
|
70
|
+
|
|
71
|
+
## Review pass before writing to disk
|
|
72
|
+
|
|
73
|
+
Read the drafted lines once more and apply, in order:
|
|
74
|
+
|
|
75
|
+
1. **The deletion test** — cut every line whose removal would change nothing about what gets built
|
|
76
|
+
2. **The word test** — replace every ornate word with a plain one, and delete every uncheckable adjective, adverb, and hedge
|
|
77
|
+
3. **The duplication test** — cut anything already stated in this file, an `import`, or a `requires` chain
|
|
78
|
+
4. **The read-aloud test** — a line you cannot read aloud in one breath is either two facts or one padded one
|
|
79
|
+
|
|
80
|
+
If a cut would remove an interface detail, a boundary case, or a disambiguation, keep the fact and shorten the wording instead — see *Do not over-cut* in `concise-specs-examples.md` for what a spec cut past its content looks like.
|
|
@@ -13,7 +13,7 @@ When writing or editing a `***definitions***` section in a `.plain` file, always
|
|
|
13
13
|
|
|
14
14
|
## Uniqueness
|
|
15
15
|
- Concept names must be globally unique across the spec and all its imports
|
|
16
|
-
- Check for collisions with
|
|
16
|
+
- Check for collisions with concepts coming from `import` and `requires` modules before adding
|
|
17
17
|
|
|
18
18
|
## Predefined concepts (do not redefine)
|
|
19
19
|
- ***plain ships several predefined concepts that are available in every module without being defined
|
|
@@ -31,6 +31,10 @@ When writing or editing a `***definitions***` section in a `.plain` file, always
|
|
|
31
31
|
| `:ConformanceTests:` | Auto-generated tests verifying conformance to the spec |
|
|
32
32
|
| `:AcceptanceTest:` / `:AcceptanceTests:` | Tests validating specific aspects of the implementation |
|
|
33
33
|
|
|
34
|
+
## Conciseness
|
|
35
|
+
- A definition is one plain sentence plus, where needed, its attributes — see `concise-specs.md`
|
|
36
|
+
- Never restate the concept name as its own definition, and never explain why the concept exists
|
|
37
|
+
|
|
34
38
|
## Define before use
|
|
35
39
|
- A concept must be defined before it is referenced in any section (definitions, implementation reqs, functional specs, test reqs)
|
|
36
40
|
- Sources of definitions: the module's own `***definitions***`, an `import`ed module's definitions, or a `require`d module's `exported_concepts`
|
|
@@ -60,8 +64,8 @@ Bad — circular:
|
|
|
60
64
|
```
|
|
61
65
|
|
|
62
66
|
## Exported concepts are not transitive
|
|
63
|
-
- If module A exports a concept and module B `requires` A, module C `requires
|
|
64
|
-
-
|
|
67
|
+
- If module A exports a concept and module B `requires` A, a module C with `requires: [B]` does **not** gain access to A's exports
|
|
68
|
+
- To use an ancestor's exports, list that ancestor in `requires` as well; concepts shared more broadly belong in a common import module
|
|
65
69
|
|
|
66
70
|
## Description quality
|
|
67
71
|
- Descriptions must be clear, concise, and language-agnostic
|
|
@@ -89,7 +93,6 @@ Example of defining technical components alongside their behavior:
|
|
|
89
93
|
|
|
90
94
|
- :CsvFileReader: reads CSV rows in a batched manner.
|
|
91
95
|
|
|
92
|
-
|
|
93
96
|
- :CsvJoiner: joins csvs and uses :CsvFileWriter: to create the merged CSV.
|
|
94
97
|
```
|
|
95
98
|
|
|
@@ -10,6 +10,7 @@ When adding or editing `exported_concepts` in a `.plain` file's frontmatter, alw
|
|
|
10
10
|
- `exported_concepts` declares which concepts from this module are visible to modules that `require` it
|
|
11
11
|
- Concepts not listed in `exported_concepts` are internal to the module and invisible to downstream modules
|
|
12
12
|
- Only modules that use `requires` receive exported concepts — `import` gives access to all definitions, not just exports
|
|
13
|
+
- An exported concept is visible with its full definition text
|
|
13
14
|
|
|
14
15
|
## When to use it
|
|
15
16
|
- Use `exported_concepts` on any module that other modules will `require`
|
|
@@ -21,8 +22,8 @@ When adding or editing `exported_concepts` in a `.plain` file's frontmatter, alw
|
|
|
21
22
|
- Do not export concepts that are not defined in the module
|
|
22
23
|
|
|
23
24
|
## Exports are not transitive
|
|
24
|
-
- If module A exports `:Foo:` and module B `requires` A, module C `requires
|
|
25
|
-
- If module C also needs `:Foo:`, it
|
|
25
|
+
- If module A exports `:Foo:` and module B `requires` A, a module C with `requires: [B]` does **not** gain access to `:Foo:`
|
|
26
|
+
- If module C also needs `:Foo:`, it should list A in its `requires` as well (`requires: [A, B]`) or get `:Foo:` through a common import module
|
|
26
27
|
|
|
27
28
|
## Format
|
|
28
29
|
|
|
@@ -37,8 +37,15 @@ GOOD — statements of fact with concrete subjects:
|
|
|
37
37
|
- A :User: can add a :Task:. Only valid :Task: items are added.
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Conciseness
|
|
41
|
+
- A functional spec that a human will not read is not reviewed, so every spec must be as short as its content allows — see `concise-specs.md`
|
|
42
|
+
- Cut anything obvious, any rationale, and every uncheckable adjective; keep every interface detail, boundary case, and disambiguation
|
|
43
|
+
- Shorten the wording, never the facts: the rules below always win over brevity
|
|
44
|
+
|
|
40
45
|
## Complexity limit
|
|
41
|
-
-
|
|
46
|
+
- The complexity limit is about breaking software into small, independently understandable behavior slices that people and AI can reason about, implement, and test reliably
|
|
47
|
+
- The current mechanism for enforcing the complexity limit is that each functional spec must imply a **maximum of 200 changed lines of code**.
|
|
48
|
+
- Smaller slices produce focused test failures, reduce hidden coupling and regression surface, and make review and incremental rendering more predictable
|
|
42
49
|
- If a spec is too large, use `break-down-func-spec` to split it into multiple smaller, independent specs
|
|
43
50
|
- Use `analyze-if-func-spec-too-complex` to verify before inserting
|
|
44
51
|
- Use `analyze-func-specs` to check a spec (or a batch of specs) against all relevant existing specs in a single batched call; use `resolve-spec-conflict` for each conflicting pair it reports
|
|
@@ -57,7 +64,7 @@ GOOD — statements of fact with concrete subjects:
|
|
|
57
64
|
- Specs are rendered incrementally, top to bottom
|
|
58
65
|
- The renderer has **no knowledge of future specs** — only previously rendered specs are in context
|
|
59
66
|
- A new spec can reference behavior from earlier specs but cannot assume anything about specs that come after it
|
|
60
|
-
- Functional specs from `requires` modules are treated as previous requirements
|
|
67
|
+
- Functional specs from `requires` modules are treated as previous requirements — transitively, across the whole ancestor chain
|
|
61
68
|
|
|
62
69
|
## No conflicts
|
|
63
70
|
- The new spec must not contradict any existing functional spec
|
|
@@ -92,7 +99,7 @@ GOOD — statements of fact with concrete subjects:
|
|
|
92
99
|
|
|
93
100
|
## Encapsulation
|
|
94
101
|
- Functionality must be self-contained in the spec text
|
|
95
|
-
- `requires` modules only
|
|
102
|
+
- `requires` modules only pass on functional specs and `exported_concepts` — never implementation reqs, so do not rely on implementation reqs to convey behavior
|
|
96
103
|
- Behavior that downstream modules need must be expressed in functional specs, not elsewhere
|
|
97
104
|
|
|
98
105
|
## Acceptance tests
|
package/forge/rules/impl-reqs.md
CHANGED
|
@@ -13,7 +13,7 @@ When writing or editing an `***implementation reqs***` section in a `.plain` fil
|
|
|
13
13
|
|
|
14
14
|
## `:UnitTests:` lives here (hard rule)
|
|
15
15
|
- **Everything** about `:UnitTests:` goes in `***implementation reqs***` — paths, approach, packages, framework, conventions, fixtures, mocking policy, file layout, naming, lint / static-analysis gates
|
|
16
|
-
- `:UnitTests:` are part of the generated codebase (they sit inside `plain_modules/<module
|
|
16
|
+
- `:UnitTests:` are part of the generated codebase (they sit inside `plain_modules/<module>/code/` alongside the implementation), so requirements that shape them are implementation reqs by definition
|
|
17
17
|
- The unit-test generator reads **only** `***implementation reqs***` — anything about `:UnitTests:` placed elsewhere (e.g. `***test reqs***`) is silently ignored
|
|
18
18
|
- Author each `:UnitTests:` requirement via `add-implementation-requirement` and phrase it in terms of `:UnitTests:` so the partition stays visible at a glance
|
|
19
19
|
|
|
@@ -41,17 +41,21 @@ When writing or editing an `***implementation reqs***` section in a `.plain` fil
|
|
|
41
41
|
- Unit testing run scripts should be linked here as a linked resource
|
|
42
42
|
|
|
43
43
|
## Encapsulation warning
|
|
44
|
-
- `requires` modules only
|
|
44
|
+
- `requires` modules only pass on functional specs and `exported_concepts` — never implementation reqs
|
|
45
45
|
- If downstream modules need certain behavior to be visible, express it in functional specs, not here
|
|
46
46
|
|
|
47
|
+
## Conciseness
|
|
48
|
+
- State each req in the fewest plain words that stay exact — see `concise-specs.md`
|
|
49
|
+
- Do not restate the standard behavior of a technology this section already names
|
|
50
|
+
|
|
47
51
|
## No duplication
|
|
48
52
|
- Do not duplicate guidance already present in the file or its imports
|
|
49
|
-
- Check imported
|
|
53
|
+
- Check imported modules before adding a new req
|
|
50
54
|
|
|
51
55
|
## Concept references
|
|
52
56
|
- Reference defined `:Concepts:` where they add clarity
|
|
53
57
|
- All referenced concepts must already be defined in `***definitions***`
|
|
54
|
-
- Implementation
|
|
58
|
+
- Make the component a req constrains its subject; `:Implementation:` is the whole codebase, so use it only for a fact that holds everywhere — language, dependency manager, stack-wide convention
|
|
55
59
|
|
|
56
60
|
## Format
|
|
57
61
|
|
|
@@ -59,6 +63,6 @@ When writing or editing an `***implementation reqs***` section in a `.plain` fil
|
|
|
59
63
|
***implementation reqs***
|
|
60
64
|
- :Implementation: should be in Python 3.12.
|
|
61
65
|
- :Implementation: should use pip for dependency management.
|
|
62
|
-
-
|
|
66
|
+
- :ReportExporter: writes a :CsvFile: with streaming writes, never holding the full dataset in memory.
|
|
63
67
|
- :UnitTests: are executed via the run script [test_scripts/run_unittests_python.sh](test_scripts/run_unittests_python.sh).
|
|
64
68
|
```
|
|
@@ -10,18 +10,16 @@ When creating or editing a `.plain` file that uses `import` or is intended to be
|
|
|
10
10
|
- An import module contains **only** `***definitions***`, `***implementation reqs***`, and/or `***test reqs***`
|
|
11
11
|
- It must **not** contain `***functional specs***`
|
|
12
12
|
- It must **not** use `requires` in its frontmatter
|
|
13
|
-
- It
|
|
14
|
-
- It may optionally `import` other modules or templates for layered reuse
|
|
13
|
+
- It may optionally `import` other import modules for layered reuse
|
|
15
14
|
|
|
16
15
|
## What import does
|
|
17
16
|
- `import` pulls in `***definitions***`, `***implementation reqs***`, and `***test reqs***` from the target module
|
|
18
17
|
- It does **not** pull in `***functional specs***`
|
|
19
|
-
- The default import directory is `template/` — the `template/` prefix is not needed in import paths
|
|
20
18
|
- Import paths omit the `.plain` extension
|
|
21
19
|
|
|
22
20
|
## Concept visibility
|
|
23
|
-
- All concepts defined in the imported module become available in the importing module
|
|
24
|
-
- Check for concept name collisions between imports and local definitions before adding
|
|
21
|
+
- All concepts defined in the imported module become available in the importing module — including concepts the imported module itself imports (layered imports are transitive)
|
|
22
|
+
- Check for concept name collisions between imports and local definitions before adding — a collision is rejected as a syntax error ("Concepts were defined multiple times")
|
|
25
23
|
|
|
26
24
|
## Format
|
|
27
25
|
|
|
@@ -46,5 +44,5 @@ import:
|
|
|
46
44
|
## Combining import with requires
|
|
47
45
|
- A module can use both `import` and `requires` together
|
|
48
46
|
- `import` brings in shared definitions and reqs
|
|
49
|
-
- `requires`
|
|
47
|
+
- `requires` attaches the module to the build chain — accumulated generated code and previous functional specs
|
|
50
48
|
- An import module itself must **not** use `requires`
|
|
@@ -75,7 +75,7 @@ mvn test -Dtest='<:UnitTests: package>.**.*Test' checkstyle:check
|
|
|
75
75
|
|
|
76
76
|
These reqs feed `run_conformance_tests_<lang>`. At minimum, declare:
|
|
77
77
|
|
|
78
|
-
1. **`:ConformanceTests:` source location** —
|
|
78
|
+
1. **`:ConformanceTests:` source location** — the generated suite lives under `plain_modules/<module>/tests/`, one folder per functional spec; the renderer passes the resolved path of the folder under test as `$2`
|
|
79
79
|
2. **`:ConformanceTests:` framework and execution command** — `mvn test --no-transfer-progress`, `pytest`, `npm test`, `go test ./...`, etc., with any flags / profiles the project requires
|
|
80
80
|
3. **Fully qualified `:ConformanceTests:` package** (or path / pattern) used to scope discovery, if the runner needs one
|
|
81
81
|
4. **`:ConformanceTests:` network and secrets policy** — by default the suite runs against the **live provider** (see [`integrations.md`](integrations.md) → *`:ConformanceTests:` always run against the live integration*). Declare the env-var names the script reads (e.g. `<PROVIDER>_API_KEY`), whether the script loads a `.env` file before running, and any specific endpoints that are mocked because they can't be exercised live safely (429, forced 5xx)
|
|
@@ -85,7 +85,7 @@ These reqs feed `run_conformance_tests_<lang>`. At minimum, declare:
|
|
|
85
85
|
Author the conformance facts as one or more entries, phrased in terms of `:ConformanceTests:`:
|
|
86
86
|
|
|
87
87
|
```plain
|
|
88
|
-
- :ConformanceTests: of :Implementation: live in `
|
|
88
|
+
- :ConformanceTests: of :Implementation: live in `plain_modules/foo/tests/` and are implemented with JUnit 5 + Maven.
|
|
89
89
|
- The fully qualified package used for :ConformanceTests: discovery is `com.example.integrations.foo.conformance`.
|
|
90
90
|
- :ConformanceTests: are run via `mvn test --no-transfer-progress`; the host's Surefire plugin must be installed.
|
|
91
91
|
- :ConformanceTests: run against the live :ProviderName: sandbox — no mocking of provider calls.
|
|
@@ -139,7 +139,7 @@ All three scripts are invoked by the `codeplain` renderer with positional argume
|
|
|
139
139
|
|
|
140
140
|
## 1. `prepare_environment_<lang>` — copy into the host, then compile
|
|
141
141
|
|
|
142
|
-
Receives one positional argument: the renderer's build output folder (e.g. `plain_modules/<module
|
|
142
|
+
Receives one positional argument: the renderer's build output folder (e.g. `plain_modules/<module>/code/`).
|
|
143
143
|
|
|
144
144
|
Purpose: stage the generated code **into the host codebase at the module's package path**, then run the host's install / build so that downstream test projects (specifically the conformance suite) can depend on it from the local dependency cache.
|
|
145
145
|
|
|
@@ -30,6 +30,9 @@ The embedded integration's `.plain` specs (and the rest of the ***plain project
|
|
|
30
30
|
│ ├── resources/
|
|
31
31
|
│ ├── test_scripts/
|
|
32
32
|
│ ├── plain_modules/ # generated; gitignored
|
|
33
|
+
│ │ └── <module>/
|
|
34
|
+
│ │ ├── code/ # implementation + unit tests
|
|
35
|
+
│ │ └── tests/ # conformance tests, per functional spec
|
|
33
36
|
│ └── config.yaml
|
|
34
37
|
└── src/ # host source tree (Java, Python, etc.)
|
|
35
38
|
```
|
|
@@ -79,7 +82,6 @@ This rule applies to **host source code only**. Other artifacts still live under
|
|
|
79
82
|
|
|
80
83
|
- **Contract schemas authored by the integration** — `resources/contract/<entry-point>.schema.json`
|
|
81
84
|
- **Configuration schema** — `resources/config.schema.json`
|
|
82
|
-
- **Captured probe responses** (from the live-API cross-check) — `resources/fixtures/<endpoint>.<case>.json`
|
|
83
85
|
- **Static lookup tables** the integration owns — `resources/error-map.yaml`, `resources/retry-policy.yaml`, etc.
|
|
84
86
|
|
|
85
87
|
The rule of thumb: if the host wrote it and ships it, link it where the host put it. If the integration is authoring it for the first time, it goes under `resources/`.
|
|
@@ -117,7 +119,7 @@ The renderer reads the directives from the spec and the shapes from the linked s
|
|
|
117
119
|
|
|
118
120
|
## Test-script wiring — copy into the host, run tests there
|
|
119
121
|
|
|
120
|
-
Embedded integrations are tested **inside the host codebase itself**. The prepare and unit-test scripts copy the renderer's output (`$1`, i.e. `plain_modules/<module
|
|
122
|
+
Embedded integrations are tested **inside the host codebase itself**. The prepare and unit-test scripts copy the renderer's output (`$1`, i.e. `plain_modules/<module>/code/`) into the host's source tree at the module's package path, then compile / test the host project in place. Only the conformance script uses a scratch folder in the system temp directory (`/tmp/<lang>_conformance/`), because the conformance suite is a separate project that consumes the host build as a dependency.
|
|
121
123
|
|
|
122
124
|
This matters because the integration's generated code references host symbols by their full import path (e.g. `from host_project.integrations.base import IntegrationContract`). Those imports only resolve cleanly when the test process is rooted in the host's package layout — anything else creates path edge cases that bite later in conformance failures.
|
|
123
125
|
|
|
@@ -103,7 +103,7 @@ Capture as a contract-version concept; pin the version in every published schema
|
|
|
103
103
|
|
|
104
104
|
`:ConformanceTests:` for a standalone integration **run against the live provider** (see [`integrations.md`](integrations.md) → *`:ConformanceTests:` always run against the live integration*). The testing strategy is captured as `***test reqs***` entries authored via `add-test-requirement`:
|
|
105
105
|
|
|
106
|
-
- **Conformance is live by default.** No VCR cassettes, no prerecorded responses for the calls under test, no mock servers. A green conformance run that never touched the provider proves nothing. Recorded responses
|
|
106
|
+
- **Conformance is live by default.** No VCR cassettes, no prerecorded responses for the calls under test, no mock servers. A green conformance run that never touched the provider proves nothing. Recorded responses may back unit tests — never conformance
|
|
107
107
|
- **Secrets come from environment variables.** Pin every credential as an env-var name (e.g. `<PROVIDER>_API_KEY`, `<PROVIDER>_CLIENT_ID` + `<PROVIDER>_CLIENT_SECRET`) in `***test reqs***` and in the auth concept. Use the names the provider's docs use so users don't have to translate
|
|
108
108
|
- **The user supplies values via `.env` or the shell.** The project ships `.env.example` (gitignored `.env` for real values). CI provides the same env-var names from its secret store. The conformance script may optionally `source` a `.env` from the project root if one exists; it must verify every required var is set after that optional load and fail fast (exit `69`) on missing vars
|
|
109
109
|
- **Sandbox credentials in CI.** Name where credentials come from (CI secret store, dedicated test tenant), the rotation / leak-response policy, and the env-var names CI must set
|