@anselmdk/feature-spec-md 0.7.0 → 0.8.1
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 +71 -20
- package/SPEC_FORMAT.md +125 -4
- package/dist/githubActionDiffReport.js +16 -26
- package/dist/githubActionDiffReport.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mockReports.js +7 -4
- package/dist/mockReports.js.map +1 -1
- package/dist/reportTemplate.d.ts.map +1 -1
- package/dist/reportTemplate.js +339 -32
- package/dist/reportTemplate.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/evidence-policy.md +4 -2
- package/package.json +1 -1
- package/src/mocks/current/screenshots/screenshots.json +6 -6
- package/src/mocks/current/specs/support-desk.design.md +32 -0
- package/src/mocks/current/specs/support-desk.model.md +22 -1
- package/src/mocks/current/specs/support-platform.stack.md +28 -0
- package/src/mocks/current/specs/ticket-inbox.feature.md +2 -0
- package/src/mocks/current/specs/ticket-reply.feature.md +4 -0
- package/src/mocks/previous/screenshots/screenshots.json +3 -3
- package/src/mocks/previous/specs/ticket-inbox.feature.md +2 -0
- package/src/mocks/previous/specs/ticket-reply.feature.md +2 -0
- package/templates/feature.feature.md +4 -2
package/README.md
CHANGED
|
@@ -7,9 +7,19 @@ Markdown specs for AI-assisted, testable spec-driven development.
|
|
|
7
7
|
```txt
|
|
8
8
|
model + features + stack + design
|
|
9
9
|
-> AI-written executable tests that reference stable spec IDs
|
|
10
|
-
-> validation, coverage, screenshots, and
|
|
10
|
+
-> validation, coverage, screenshots, reports, and PR diffs
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## See the workflow in action
|
|
14
|
+
|
|
15
|
+
The clearest example of what this library can deliver is the demo feature PR workflow:
|
|
16
|
+
|
|
17
|
+
- **Feature PR:** <https://github.com/anselmdk/feature-spec-md-demo/pull/16>
|
|
18
|
+
- **Feature spec report:** <https://feature-spec-md.anselm.dk/demo/build/269/>
|
|
19
|
+
- **Feature spec diff:** <https://feature-spec-md.anselm.dk/demo/pr/16/269/>
|
|
20
|
+
|
|
21
|
+
PRs like this are the essence of `feature-spec-md`: a feature branch can carry the product/spec/code change, a published feature spec report can show the full implementation state, and a PR diff report can show exactly what changed in the specs, rendered report, and screenshot evidence.
|
|
22
|
+
|
|
13
23
|
## What the tool does
|
|
14
24
|
|
|
15
25
|
The shortest version is: `feature-spec-md` turns Markdown specifications into a testable contract.
|
|
@@ -21,21 +31,23 @@ It gives you:
|
|
|
21
31
|
- **Validation** that checks frontmatter, headings, IDs, cross-document references, and test coverage expectations.
|
|
22
32
|
- **Coverage reporting** that shows which model items, rules, and scenarios are implemented by tests.
|
|
23
33
|
- **Scenario evidence policy** so specs can declare whether behavior should be tested by unit, integration, Playwright, manual, or no executable tests.
|
|
24
|
-
- **HTML reports** that
|
|
34
|
+
- **HTML reports** that combine specs, coverage, validation status, source links, GitHub/build metadata, and screenshot evidence.
|
|
35
|
+
- **PR diff reports** that compare a current published build with a base or previous build and highlight changed report files, changed spec sections, and screenshot evidence.
|
|
25
36
|
- **Screenshot evidence** for scenarios when using the Playwright helper.
|
|
26
|
-
- **
|
|
37
|
+
- **GitHub Actions publishing helpers** for job summaries, FTP-published build reports, FTP-published PR diff reports, and PR comments.
|
|
38
|
+
- **A library API** for projects that want to parse specs, check coverage, collect screenshots, render reports, or render diff reports from their own tooling.
|
|
27
39
|
|
|
28
40
|
The specs are meant to be written with an AI before implementation. The tests are meant to be written with an AI from those specs. `feature-spec-md` then checks that the Markdown stays structured and that executable tests still cover the model items, rules, and scenarios the specs define.
|
|
29
41
|
|
|
30
42
|
## Demo project
|
|
31
43
|
|
|
32
|
-
A complete demo app is available in [`anselmdk/feature-spec-md-demo`](https://github.com/anselmdk/feature-spec-md-demo). It shows a small support-ticket desk built from model, feature, stack, and design specs, with unit tests, Playwright tests, generated coverage, and a published report.
|
|
44
|
+
A complete demo app is available in [`anselmdk/feature-spec-md-demo`](https://github.com/anselmdk/feature-spec-md-demo). It shows a small support-ticket desk built from model, feature, stack, and design specs, with unit tests, Playwright tests, generated coverage, screenshot evidence, a published feature spec report, and a published PR diff report.
|
|
33
45
|
|
|
34
46
|
Demo reports, including scenario screenshots, are available here:
|
|
35
47
|
|
|
36
48
|
<https://feature-spec-md.anselm.dk/demo/>
|
|
37
49
|
|
|
38
|
-
Use the demo repository when you want to see the expected project shape, script names, screenshot manifest flow, and report output in a real app.
|
|
50
|
+
Use the demo repository when you want to see the expected project shape, script names, screenshot manifest flow, CI publishing setup, PR comments, and report output in a real app.
|
|
39
51
|
|
|
40
52
|
## What you write
|
|
41
53
|
|
|
@@ -62,9 +74,9 @@ A person who has completed registration and can request sign-in links.
|
|
|
62
74
|
|
|
63
75
|
### Feature specs
|
|
64
76
|
|
|
65
|
-
Feature specs describe user-facing behavior with rules and scenarios. Rules and scenarios get stable IDs that tests can reference.
|
|
77
|
+
Feature specs describe user-facing behavior with rules and scenarios. Rules and scenarios get stable IDs that tests can reference. Scenario steps are fenced so Markdown previews preserve line breaks without relying on trailing spaces.
|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
````md
|
|
68
80
|
---
|
|
69
81
|
id: ACCOUNT-ACCESS
|
|
70
82
|
title: Account access
|
|
@@ -86,10 +98,12 @@ Allow registered people to access their account securely.
|
|
|
86
98
|
|
|
87
99
|
### ACCOUNT-ACCESS-S001: Registered person signs in
|
|
88
100
|
|
|
101
|
+
```
|
|
89
102
|
Given a registered person is on the sign-in page
|
|
90
103
|
When they request and open a valid sign-in link
|
|
91
104
|
Then they are signed in
|
|
92
105
|
```
|
|
106
|
+
````
|
|
93
107
|
|
|
94
108
|
Feature specs can declare the expected test and evidence policy:
|
|
95
109
|
|
|
@@ -98,17 +112,19 @@ test: unit | integration | playwright | manual | skip
|
|
|
98
112
|
screenshots: required | optional | skip
|
|
99
113
|
```
|
|
100
114
|
|
|
101
|
-
If `screenshots` is omitted, Playwright scenarios default to `required`; non-Playwright scenarios default to `skip`. Scenario-level overrides can be written directly below a scenario heading:
|
|
115
|
+
If `screenshots` is omitted, Playwright scenarios default to `required`; non-Playwright scenarios default to `skip`. Scenario-level overrides can be written directly below a scenario heading, before the fenced scenario block:
|
|
102
116
|
|
|
103
|
-
|
|
117
|
+
````md
|
|
104
118
|
### ACCOUNT-ACCESS-S002: Link expiry is calculated
|
|
105
119
|
Test: unit
|
|
106
120
|
Screenshots: skip
|
|
107
121
|
|
|
122
|
+
```
|
|
108
123
|
Given a sign-in link was created 31 minutes ago
|
|
109
124
|
When expiry is calculated
|
|
110
125
|
Then the link is expired
|
|
111
126
|
```
|
|
127
|
+
````
|
|
112
128
|
|
|
113
129
|
See [SPEC_FORMAT.md](SPEC_FORMAT.md) and [docs/evidence-policy.md](docs/evidence-policy.md) for the exact format.
|
|
114
130
|
|
|
@@ -155,7 +171,8 @@ npx feature-spec-md init --kind design --dir specs
|
|
|
155
171
|
4. Ask an AI to write executable tests from the specs, preserving the relevant `-M001`, `-R001`, and `-S001` IDs in the test source.
|
|
156
172
|
5. Run `npx feature-spec-md coverage` to see which scenarios, rules, and model items have tests.
|
|
157
173
|
6. Run `npx feature-spec-md report` to generate an HTML implementation report for review or CI artifacts.
|
|
158
|
-
7. Use `npx feature-spec-md github-report` in GitHub Actions when the report should be linked from the job summary or published.
|
|
174
|
+
7. Use `npx feature-spec-md github-report` in GitHub Actions when the report should be linked from the job summary, uploaded as an artifact, or published by FTP.
|
|
175
|
+
8. Use `npx feature-spec-md github-diff-report` in PR builds when a published diff should compare the current report output and screenshots with a base build and provide a PR comment body.
|
|
159
176
|
|
|
160
177
|
The longer flow, including AI prompts and CI setup, is in [docs/spec-driven-flow.md](docs/spec-driven-flow.md). The demo repository also shows the flow in practice: <https://github.com/anselmdk/feature-spec-md-demo>.
|
|
161
178
|
|
|
@@ -164,8 +181,9 @@ The longer flow, including AI prompts and CI setup, is in [docs/spec-driven-flow
|
|
|
164
181
|
```bash
|
|
165
182
|
npx feature-spec-md check
|
|
166
183
|
npx feature-spec-md coverage --fail-on-missing
|
|
167
|
-
npx feature-spec-md report --out test-results/
|
|
168
|
-
npx feature-spec-md github-report --report-dir test-results/spec-report
|
|
184
|
+
npx feature-spec-md report --out test-results/spec-report/index.html
|
|
185
|
+
npx feature-spec-md github-report --report-dir test-results/spec-report --publish ftp
|
|
186
|
+
npx feature-spec-md github-diff-report --publish ftp --pr-number 123
|
|
169
187
|
```
|
|
170
188
|
|
|
171
189
|
By default the CLI scans:
|
|
@@ -175,7 +193,7 @@ specs/**/*.model.md
|
|
|
175
193
|
specs/**/*.feature.md
|
|
176
194
|
specs/**/*.stack.md
|
|
177
195
|
specs/**/*.design.md
|
|
178
|
-
tests/**/*.
|
|
196
|
+
tests/**/*.ts
|
|
179
197
|
```
|
|
180
198
|
|
|
181
199
|
Use explicit patterns when your project uses different paths:
|
|
@@ -188,13 +206,13 @@ npx feature-spec-md check \
|
|
|
188
206
|
|
|
189
207
|
### `check`
|
|
190
208
|
|
|
191
|
-
Validates the spec set and, by default, requires scenario coverage when tests are scanned.
|
|
209
|
+
Validates the spec set and, by default, requires model, rule, and scenario coverage when tests are scanned.
|
|
192
210
|
|
|
193
211
|
```bash
|
|
194
212
|
npx feature-spec-md check
|
|
195
213
|
```
|
|
196
214
|
|
|
197
|
-
`check` validates spec structure, references between documents, and test coverage. Use `--require-scenario-coverage=false
|
|
215
|
+
`check` validates spec structure, references between documents, and test coverage. Use `--require-scenario-coverage=false`, `--require-rule-coverage=false`, or `--require-model-coverage=false` while drafting.
|
|
198
216
|
|
|
199
217
|
### `coverage`
|
|
200
218
|
|
|
@@ -204,7 +222,7 @@ Prints a terminal implementation report showing covered and missing model items,
|
|
|
204
222
|
npx feature-spec-md coverage --fail-on-missing
|
|
205
223
|
```
|
|
206
224
|
|
|
207
|
-
Use `--fail-on-missing` when missing model item, rule, or scenario coverage should fail CI.
|
|
225
|
+
Use `--fail-on-missing=false` when missing model item, rule, or scenario coverage should not fail CI.
|
|
208
226
|
|
|
209
227
|
### `report`
|
|
210
228
|
|
|
@@ -223,7 +241,7 @@ npx feature-spec-md report \
|
|
|
223
241
|
--screenshots "test-results/spec-report/screenshots-*.json"
|
|
224
242
|
```
|
|
225
243
|
|
|
226
|
-
When CI should fail for missing declared screenshot evidence,
|
|
244
|
+
When CI should fail for missing declared screenshot evidence, use `--enforce-evidence`:
|
|
227
245
|
|
|
228
246
|
```bash
|
|
229
247
|
npx feature-spec-md report \
|
|
@@ -232,7 +250,7 @@ npx feature-spec-md report \
|
|
|
232
250
|
--out test-results/spec-report/index.html
|
|
233
251
|
```
|
|
234
252
|
|
|
235
|
-
This gate only fails for scenarios whose resolved screenshot policy is `required`.
|
|
253
|
+
This gate only fails for scenarios whose resolved screenshot policy is `required`. In GitHub Actions, the report also includes source links and report metadata derived from the repository, ref, SHA, run, build number, and pull request context when available.
|
|
236
254
|
|
|
237
255
|
### `github-report`
|
|
238
256
|
|
|
@@ -244,6 +262,20 @@ npx feature-spec-md github-report \
|
|
|
244
262
|
--publish ftp
|
|
245
263
|
```
|
|
246
264
|
|
|
265
|
+
With FTP publishing, build reports are published below a `build/<build-number>/` directory. This keeps immutable build outputs available for later PR diff comparisons.
|
|
266
|
+
|
|
267
|
+
### `github-diff-report`
|
|
268
|
+
|
|
269
|
+
Builds and publishes a PR diff report from already-published feature spec report outputs.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npx feature-spec-md github-diff-report \
|
|
273
|
+
--publish ftp \
|
|
274
|
+
--pr-number 123
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The diff report lists changed report assets, extracts changed spec sections, groups screenshot changes by spec/scenario, writes a GitHub Actions summary, and exposes a `diff-comment-body` output that a workflow can add to the PR. PR diff reports are published below `pr/<pr-number>/<build-number>/`.
|
|
278
|
+
|
|
247
279
|
## Playwright screenshot evidence
|
|
248
280
|
|
|
249
281
|
The package exports a Playwright helper from `@anselmdk/feature-spec-md/playwright`. It maps scenario step text back to the spec line, wraps the implementation in a Playwright `test.step`, captures a screenshot after the step, attaches it to the test, and writes a screenshot manifest such as `test-results/spec-report/screenshots-0.json`.
|
|
@@ -252,7 +284,7 @@ That manifest can then be passed to `feature-spec-md report` with `--screenshots
|
|
|
252
284
|
|
|
253
285
|
## GitHub Actions report publishing
|
|
254
286
|
|
|
255
|
-
`feature-spec-md github-report` writes the GitHub Actions job summary and can either prepare outputs for a GitHub artifact upload or publish the generated report to FTP.
|
|
287
|
+
`feature-spec-md github-report` writes the GitHub Actions job summary and can either prepare outputs for a GitHub artifact upload or publish the generated report to FTP. `feature-spec-md github-diff-report` compares published build reports and publishes a PR-specific diff report.
|
|
256
288
|
|
|
257
289
|
For FTP publishing, configure repository secrets in GitHub under **Settings → Secrets and variables → Actions → Repository secrets**.
|
|
258
290
|
|
|
@@ -271,9 +303,12 @@ Optional values:
|
|
|
271
303
|
FEATURE_SPEC_FTP_REMOTE_DIR=/public_html/feature-spec-md
|
|
272
304
|
FEATURE_SPEC_FTP_PORT=21
|
|
273
305
|
FEATURE_SPEC_FTP_SECURE=false
|
|
306
|
+
FEATURE_SPEC_BUILD_NUMBER=<build number, defaults to GITHUB_RUN_NUMBER>
|
|
307
|
+
FEATURE_SPEC_PR_NUMBER=<pull request number>
|
|
308
|
+
FEATURE_SPEC_BASE_BUILD_NUMBER=<main/base build number for PR diffs>
|
|
274
309
|
```
|
|
275
310
|
|
|
276
|
-
Example GitHub Actions step:
|
|
311
|
+
Example GitHub Actions step for publishing the current report:
|
|
277
312
|
|
|
278
313
|
```yaml
|
|
279
314
|
- name: Publish feature spec report
|
|
@@ -286,6 +321,19 @@ Example GitHub Actions step:
|
|
|
286
321
|
FEATURE_SPEC_REPORT_BASE_URL: ${{ secrets.FEATURE_SPEC_REPORT_BASE_URL }}
|
|
287
322
|
```
|
|
288
323
|
|
|
324
|
+
Example step for publishing a PR diff after the current report has been published:
|
|
325
|
+
|
|
326
|
+
```yaml
|
|
327
|
+
- name: Publish feature spec PR diff
|
|
328
|
+
if: always() && github.event_name == 'pull_request'
|
|
329
|
+
run: npx feature-spec-md github-diff-report --publish ftp --pr-number "${{ github.event.pull_request.number }}"
|
|
330
|
+
env:
|
|
331
|
+
FEATURE_SPEC_FTP_HOST: ${{ secrets.FEATURE_SPEC_FTP_HOST }}
|
|
332
|
+
FEATURE_SPEC_FTP_USER: ${{ secrets.FEATURE_SPEC_FTP_USER }}
|
|
333
|
+
FEATURE_SPEC_FTP_PASSWORD: ${{ secrets.FEATURE_SPEC_FTP_PASSWORD }}
|
|
334
|
+
FEATURE_SPEC_REPORT_BASE_URL: ${{ secrets.FEATURE_SPEC_REPORT_BASE_URL }}
|
|
335
|
+
```
|
|
336
|
+
|
|
289
337
|
## Library API
|
|
290
338
|
|
|
291
339
|
Most integrations can use the top-level document API:
|
|
@@ -296,6 +344,7 @@ import {
|
|
|
296
344
|
collectSpecScreenshots,
|
|
297
345
|
parseSpecDocument,
|
|
298
346
|
renderHtmlReport,
|
|
347
|
+
renderLocalDiffReport,
|
|
299
348
|
validateScenarioScreenshots,
|
|
300
349
|
validateSpecDocument,
|
|
301
350
|
} from "@anselmdk/feature-spec-md";
|
|
@@ -309,6 +358,8 @@ Useful exports include:
|
|
|
309
358
|
- `buildSpecCoverageSummary` and `collectSpecTestReferences` for custom coverage workflows.
|
|
310
359
|
- `collectSpecScreenshots` and `validateScenarioScreenshots` for loading and enforcing screenshot evidence.
|
|
311
360
|
- `renderHtmlReport` for generating the same report UI from your own integration.
|
|
361
|
+
- `renderLocalDiffReport` for generating the same diff-report UI from local report directories.
|
|
362
|
+
- `insertReportMetadata` and `githubReportMetadata` for adding source/build/PR metadata to reports.
|
|
312
363
|
- `writeTextFile` for small report-writing integrations.
|
|
313
364
|
|
|
314
365
|
Feature-only helpers such as `parseFeatureSpec` and `checkFeatureSpecs` remain available for compatibility.
|
package/SPEC_FORMAT.md
CHANGED
|
@@ -20,6 +20,8 @@ A spec set can contain four document types:
|
|
|
20
20
|
- `*.stack.md` defines technical platform choices and their rationale.
|
|
21
21
|
- `*.design.md` defines product, UI, layout, visual, and interaction direction.
|
|
22
22
|
|
|
23
|
+
For larger products, each document type can also use the optional extension sections described below. These sections are designed for API-first products, SaaS authorization models, stateful domain objects, and e2e test environments without adding new required document suffixes.
|
|
24
|
+
|
|
23
25
|
## Frontmatter
|
|
24
26
|
|
|
25
27
|
Every file starts with YAML-like frontmatter:
|
|
@@ -73,6 +75,7 @@ Optional sections:
|
|
|
73
75
|
|
|
74
76
|
```md
|
|
75
77
|
## Rules
|
|
78
|
+
## Model Diagram
|
|
76
79
|
```
|
|
77
80
|
|
|
78
81
|
Model items use stable `-M001` IDs:
|
|
@@ -89,6 +92,22 @@ Model rules use stable `-R001` IDs and describe global invariants for the domain
|
|
|
89
92
|
- KANBAN-R001: A card MUST have one current workflow state.
|
|
90
93
|
```
|
|
91
94
|
|
|
95
|
+
### Model Diagram
|
|
96
|
+
|
|
97
|
+
Use `## Model Diagram` for one or more Mermaid diagrams that give a visual overview of model relationships. Diagrams supplement the stable model-item catalogue; they do not replace `## Model` items or affect coverage.
|
|
98
|
+
|
|
99
|
+
````md
|
|
100
|
+
## Model Diagram
|
|
101
|
+
|
|
102
|
+
```mermaid
|
|
103
|
+
erDiagram
|
|
104
|
+
BOARD ||--o{ CARD : contains
|
|
105
|
+
CARD }o--|| COLUMN : belongs_to
|
|
106
|
+
```
|
|
107
|
+
````
|
|
108
|
+
|
|
109
|
+
Generated reports render `mermaid` fenced code blocks as diagrams. If Mermaid cannot load or a diagram is invalid, the escaped diagram source remains visible in the report. Model nodes SHOULD use recognizable names corresponding to the stable model items.
|
|
110
|
+
|
|
92
111
|
## Feature Files
|
|
93
112
|
|
|
94
113
|
Feature files use the `.feature.md` suffix.
|
|
@@ -111,15 +130,17 @@ Rules use stable `-R001` IDs:
|
|
|
111
130
|
- KANBAN-CARD-AUTHORING-R001: A new card MUST start in the To do column.
|
|
112
131
|
```
|
|
113
132
|
|
|
114
|
-
Scenarios use stable `-S001` IDs and Given / When / Then steps:
|
|
133
|
+
Scenarios use stable `-S001` IDs and Given / When / Then steps. The scenario body MUST be written inside a fenced code block so Markdown previews preserve line breaks without relying on trailing spaces:
|
|
115
134
|
|
|
116
|
-
|
|
135
|
+
````md
|
|
117
136
|
### KANBAN-CARD-AUTHORING-S001: User creates a card
|
|
118
137
|
|
|
138
|
+
```
|
|
119
139
|
Given the user is on the board
|
|
120
140
|
When they create a card with the title "Write release notes"
|
|
121
141
|
Then the card "Write release notes" is visible in the To do column
|
|
122
142
|
```
|
|
143
|
+
````
|
|
123
144
|
|
|
124
145
|
Allowed step keywords are `Given`, `When`, `Then`, `And`, and `But`.
|
|
125
146
|
|
|
@@ -140,17 +161,19 @@ screenshots: required
|
|
|
140
161
|
---
|
|
141
162
|
```
|
|
142
163
|
|
|
143
|
-
Scenario-level overrides go directly below a scenario heading:
|
|
164
|
+
Scenario-level overrides go directly below a scenario heading. The Given / When / Then steps remain inside the fenced scenario block:
|
|
144
165
|
|
|
145
|
-
|
|
166
|
+
````md
|
|
146
167
|
### KANBAN-CARD-AUTHORING-S002: Card title is normalized
|
|
147
168
|
Test: unit
|
|
148
169
|
Screenshots: skip
|
|
149
170
|
|
|
171
|
+
```
|
|
150
172
|
Given the raw card title contains leading whitespace
|
|
151
173
|
When the title is normalized
|
|
152
174
|
Then the stored title has no leading whitespace
|
|
153
175
|
```
|
|
176
|
+
````
|
|
154
177
|
|
|
155
178
|
Supported `test` values are `unit`, `integration`, `playwright`, `manual`, and `skip`.
|
|
156
179
|
|
|
@@ -196,6 +219,102 @@ Required sections:
|
|
|
196
219
|
|
|
197
220
|
Optional sections are `## Principles`, `## Layout`, `## Interaction`, and `## Visual style`.
|
|
198
221
|
|
|
222
|
+
## Extension Sections
|
|
223
|
+
|
|
224
|
+
The following optional sections are valid in model, feature, stack, and design files. They are intentionally plain Markdown so teams can adopt them gradually without changing the core scenario coverage model.
|
|
225
|
+
|
|
226
|
+
Mermaid fenced code blocks are supported inside extension sections. Prefer `## Model Diagram` for domain relationship overviews and `## Lifecycle` for state diagrams.
|
|
227
|
+
|
|
228
|
+
### Open Questions
|
|
229
|
+
|
|
230
|
+
Use `## Open Questions` for unresolved product, technical, or testing choices that block confident implementation.
|
|
231
|
+
|
|
232
|
+
```md
|
|
233
|
+
## Open Questions
|
|
234
|
+
|
|
235
|
+
- BOOKING-Q001: Confirm whether attendees can buy individual drop-in classes.
|
|
236
|
+
- BOOKING-Q002: Confirm whether "user" includes public attendees or only staff members.
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Open question IDs SHOULD use the document id plus `-Q001`. They are not treated as executable behavior and do not require test coverage.
|
|
240
|
+
|
|
241
|
+
### Assumptions
|
|
242
|
+
|
|
243
|
+
Use `## Assumptions` for temporary truths the spec relies on until they are confirmed, replaced, or promoted into rules.
|
|
244
|
+
|
|
245
|
+
```md
|
|
246
|
+
## Assumptions
|
|
247
|
+
|
|
248
|
+
- BOOKING-A001: MobilePay is the first live payment provider.
|
|
249
|
+
- BOOKING-A002: All initial recurring events repeat weekly.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Assumption IDs SHOULD use the document id plus `-A001`. When an assumption becomes durable product behavior, rewrite it as a rule with a `-R001` id.
|
|
253
|
+
|
|
254
|
+
### API Contract
|
|
255
|
+
|
|
256
|
+
Use `## API Contract` for API-first features and services. This section SHOULD describe endpoints, auth requirements, request/response shapes, status codes, idempotency expectations, pagination, webhooks, and links to generated OpenAPI or Swagger output.
|
|
257
|
+
|
|
258
|
+
```md
|
|
259
|
+
## API Contract
|
|
260
|
+
|
|
261
|
+
| Method | Path | Purpose | Auth |
|
|
262
|
+
| --- | --- | --- | --- |
|
|
263
|
+
| POST | /api/events | Create an event | administrator |
|
|
264
|
+
| GET | /api/events | List visible events | account member |
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
API contract details can be covered by integration tests that reference the relevant scenario and rule IDs.
|
|
268
|
+
|
|
269
|
+
### Permissions
|
|
270
|
+
|
|
271
|
+
Use `## Permissions` for role, group, and tenant capability matrices.
|
|
272
|
+
|
|
273
|
+
```md
|
|
274
|
+
## Permissions
|
|
275
|
+
|
|
276
|
+
| Capability | Owner | Administrator | Teacher | Attendee |
|
|
277
|
+
| --- | --- | --- | --- | --- |
|
|
278
|
+
| Manage payment connection | yes | no | no | no |
|
|
279
|
+
| Move own class instance | yes | yes | yes | no |
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Permissions that must be enforced by the implementation SHOULD also be written as rules so coverage can track them.
|
|
283
|
+
|
|
284
|
+
### Lifecycle
|
|
285
|
+
|
|
286
|
+
Use `## Lifecycle` for state machines and transition rules for entities such as bookings, payments, subscriptions, passes, and recurring event instances.
|
|
287
|
+
|
|
288
|
+
```md
|
|
289
|
+
## Lifecycle
|
|
290
|
+
|
|
291
|
+
Payment states:
|
|
292
|
+
|
|
293
|
+
```txt
|
|
294
|
+
pending -> paid
|
|
295
|
+
pending -> failed
|
|
296
|
+
paid -> refunded
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Invalid transitions MUST be rejected by the API.
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Lifecycle behavior that must be executable SHOULD be represented by rules and scenarios in the same feature file.
|
|
303
|
+
|
|
304
|
+
### Test Environment
|
|
305
|
+
|
|
306
|
+
Use `## Test Environment` for mocks, seed data, fixed-time behavior, provider adapters, and CI/e2e setup.
|
|
307
|
+
|
|
308
|
+
```md
|
|
309
|
+
## Test Environment
|
|
310
|
+
|
|
311
|
+
- Email delivery is mocked and exposes the latest magic link to tests.
|
|
312
|
+
- MobilePay runs through a deterministic mock in CI.
|
|
313
|
+
- Time is frozen during recurring-event generation tests.
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
This section is especially useful for SaaS apps with external dependencies such as payment providers, email, calendars, or third-party APIs.
|
|
317
|
+
|
|
199
318
|
## Splitting Guidance
|
|
200
319
|
|
|
201
320
|
Split model files by coherent domain vocabulary, ownership, or lifecycle.
|
|
@@ -206,6 +325,8 @@ Use stack files for broad technical choices such as framework, language, testing
|
|
|
206
325
|
|
|
207
326
|
Use design files for product/UI direction such as layout, interaction, visual style, and design principles.
|
|
208
327
|
|
|
328
|
+
Use extension sections when the information belongs with a spec but is not itself a model item, durable rule, or executable scenario. If an extension section becomes large enough to hide the core behavior, split the surrounding model or feature document instead of creating a new document type.
|
|
329
|
+
|
|
209
330
|
Keep each document small enough for an AI to read, revise, and use as test-writing context without extra explanation.
|
|
210
331
|
|
|
211
332
|
## Test Coverage Convention
|
|
@@ -31,7 +31,7 @@ export async function publishGithubActionDiffReport(options) {
|
|
|
31
31
|
await writeGithubSummary([
|
|
32
32
|
"## Feature Spec PR Diff",
|
|
33
33
|
"",
|
|
34
|
-
`<p><strong>Diff report:</strong> <a href
|
|
34
|
+
`<p><strong>Diff report:</strong> <a href="${html(reportUrl)}">PR #${html(prNumber)} build ${html(currentBuild)}</a></p>`,
|
|
35
35
|
summaryComparisonSentence(report),
|
|
36
36
|
"",
|
|
37
37
|
].join("\n"));
|
|
@@ -140,12 +140,11 @@ async function compareLocalFiles(previousDir, currentDir) {
|
|
|
140
140
|
const previousSet = new Set(previousFiles);
|
|
141
141
|
const currentSet = new Set(currentFiles);
|
|
142
142
|
const allPaths = Array.from(new Set([...previousSet, ...currentSet])).sort();
|
|
143
|
-
|
|
144
|
-
for (const filePath of allPaths) {
|
|
143
|
+
return Promise.all(allPaths.map(async (filePath) => {
|
|
145
144
|
const previousInfo = previousSet.has(filePath) ? await fileInfo(join(previousDir, filePath)) : undefined;
|
|
146
145
|
const currentInfo = currentSet.has(filePath) ? await fileInfo(join(currentDir, filePath)) : undefined;
|
|
147
146
|
const status = !previousInfo ? "added" : !currentInfo ? "removed" : previousInfo.hash === currentInfo.hash ? "unchanged" : "changed";
|
|
148
|
-
|
|
147
|
+
return {
|
|
149
148
|
path: filePath,
|
|
150
149
|
kind: fileKind(filePath),
|
|
151
150
|
status,
|
|
@@ -153,9 +152,8 @@ async function compareLocalFiles(previousDir, currentDir) {
|
|
|
153
152
|
currentHash: currentInfo?.hash,
|
|
154
153
|
previousSize: previousInfo?.size,
|
|
155
154
|
currentSize: currentInfo?.size,
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return files;
|
|
155
|
+
};
|
|
156
|
+
}));
|
|
159
157
|
}
|
|
160
158
|
async function listLocalFilesRecursive(root) {
|
|
161
159
|
const files = [];
|
|
@@ -172,12 +170,10 @@ async function listLocalFilesRecursive(root) {
|
|
|
172
170
|
}
|
|
173
171
|
for (const entry of entries) {
|
|
174
172
|
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
175
|
-
if (entry.isDirectory())
|
|
173
|
+
if (entry.isDirectory())
|
|
176
174
|
await visit(relativePath);
|
|
177
|
-
|
|
178
|
-
else if (entry.isFile()) {
|
|
175
|
+
else if (entry.isFile())
|
|
179
176
|
files.push(relativePath);
|
|
180
|
-
}
|
|
181
177
|
}
|
|
182
178
|
}
|
|
183
179
|
}
|
|
@@ -190,7 +186,7 @@ async function loadSpecSections(root) {
|
|
|
190
186
|
}
|
|
191
187
|
async function loadPublishedSpecSections(root) {
|
|
192
188
|
const specRoot = join(root, publishedSpecRoot);
|
|
193
|
-
const files = (await listLocalFilesRecursive(specRoot)).filter(
|
|
189
|
+
const files = (await listLocalFilesRecursive(specRoot)).filter(isSpecMarkdownFile);
|
|
194
190
|
const sections = [];
|
|
195
191
|
for (const file of files) {
|
|
196
192
|
const source = await readFile(join(specRoot, file), "utf8");
|
|
@@ -201,6 +197,9 @@ async function loadPublishedSpecSections(root) {
|
|
|
201
197
|
}
|
|
202
198
|
return sections.sort((a, b) => a.key.localeCompare(b.key));
|
|
203
199
|
}
|
|
200
|
+
function isSpecMarkdownFile(filePath) {
|
|
201
|
+
return filePath.endsWith(".md");
|
|
202
|
+
}
|
|
204
203
|
function previousBuildNumber(builds, currentBuild) {
|
|
205
204
|
const current = Number(currentBuild);
|
|
206
205
|
return builds.map(Number).filter((build) => Number.isFinite(build) && build < current).sort((a, b) => b - a).at(0)?.toString();
|
|
@@ -253,13 +252,7 @@ function extractSpecSections(source) {
|
|
|
253
252
|
const scenarioIds = Array.from(fragment.matchAll(/<summary><code>([^<]+)<\/code>/g), (item) => textContent(item[1] ?? ""));
|
|
254
253
|
const text = specText(fragment);
|
|
255
254
|
const filePath = filePaths[0];
|
|
256
|
-
sections.push({
|
|
257
|
-
key: filePath ?? title,
|
|
258
|
-
title,
|
|
259
|
-
filePath,
|
|
260
|
-
scenarioIds,
|
|
261
|
-
text,
|
|
262
|
-
});
|
|
255
|
+
sections.push({ key: filePath ?? title, title, filePath, scenarioIds, text });
|
|
263
256
|
}
|
|
264
257
|
return sections;
|
|
265
258
|
}
|
|
@@ -350,9 +343,8 @@ function compactContext(lines) {
|
|
|
350
343
|
function scenarioSpecMap(specs) {
|
|
351
344
|
const map = new Map();
|
|
352
345
|
for (const spec of specs) {
|
|
353
|
-
for (const scenarioId of spec.scenarioIds)
|
|
346
|
+
for (const scenarioId of spec.scenarioIds)
|
|
354
347
|
map.set(scenarioId, spec);
|
|
355
|
-
}
|
|
356
348
|
}
|
|
357
349
|
return map;
|
|
358
350
|
}
|
|
@@ -384,9 +376,7 @@ function screenshotTitle(filePath, scenarioId) {
|
|
|
384
376
|
return scenarioId ? name.replace(`${scenarioId}-`, `${scenarioId} `) : name;
|
|
385
377
|
}
|
|
386
378
|
function relativeAssetUrl(prefix, filePath) {
|
|
387
|
-
return [prefix.replace(/\/+$/, ""), ...filePath.split("/").filter(Boolean)]
|
|
388
|
-
.filter(Boolean)
|
|
389
|
-
.join("/");
|
|
379
|
+
return [prefix.replace(/\/+$/, ""), ...filePath.split("/").filter(Boolean)].filter(Boolean).join("/");
|
|
390
380
|
}
|
|
391
381
|
function renderDiffReport(report) {
|
|
392
382
|
const changed = report.files.filter((file) => file.status !== "unchanged" && file.kind !== "report");
|
|
@@ -425,8 +415,8 @@ th{background:#f6f8fa}a{color:#0969da}
|
|
|
425
415
|
}
|
|
426
416
|
function renderSpecDiffs(specDiffs) {
|
|
427
417
|
if (!specDiffs.length)
|
|
428
|
-
return `<section class="panel"><h2>Spec changes</h2><p class="muted">No
|
|
429
|
-
return `<section class="panel"><h2>Spec changes</h2><p class="muted">Diffed from published
|
|
418
|
+
return `<section class="panel"><h2>Spec changes</h2><p class="muted">No spec source changes detected.</p></section>`;
|
|
419
|
+
return `<section class="panel"><h2>Spec changes</h2><p class="muted">Diffed from published Markdown files under the specs directory, not from rendered report HTML.</p>${specDiffs.map(renderSpecDiff).join("\n")}</section>`;
|
|
430
420
|
}
|
|
431
421
|
function renderSpecDiff(diff) {
|
|
432
422
|
return `<details open><summary><strong>${html(diff.filePath ?? diff.title)}</strong> <span class="badge ${diff.status}">${html(diff.status)}</span></summary><table class="diff"><tbody>${diff.lines.map(renderDiffLine).join("")}</tbody></table></details>`;
|