@anselmdk/feature-spec-md 0.1.0
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/LICENSE +21 -0
- package/README.md +54 -0
- package/SPEC_FORMAT.md +105 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +136 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +296 -0
- package/dist/index.js.map +1 -0
- package/examples/account-access.feature.md +33 -0
- package/package.json +50 -0
- package/templates/feature.feature.md +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anselm Christophersen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the Software), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# feature-spec-md
|
|
2
|
+
|
|
3
|
+
Markdown feature specs with stable rule and scenario IDs, validation, coverage checks, and generated reports.
|
|
4
|
+
|
|
5
|
+
The goal is simple:
|
|
6
|
+
|
|
7
|
+
```txt
|
|
8
|
+
human-readable feature spec
|
|
9
|
+
→ exact executable tests
|
|
10
|
+
→ generated coverage report
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The handwritten spec stays clean. It contains purpose, rules, and scenarios. Test mapping is derived from stable IDs instead of being written manually in the spec.
|
|
14
|
+
|
|
15
|
+
## Format
|
|
16
|
+
|
|
17
|
+
Feature specs are ordinary Markdown files named `*.feature.md`.
|
|
18
|
+
|
|
19
|
+
They use:
|
|
20
|
+
|
|
21
|
+
- frontmatter for metadata
|
|
22
|
+
- `## Purpose` for intent
|
|
23
|
+
- `## Rules` for general business rules
|
|
24
|
+
- `## Scenarios` for Given / When / Then examples
|
|
25
|
+
|
|
26
|
+
See `SPEC_FORMAT.md` and `examples/account-access.feature.md`.
|
|
27
|
+
|
|
28
|
+
## CLI
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install
|
|
32
|
+
npm run dev -- check --specs "examples/**/*.feature.md" --tests "tests/**/*.test.ts" --require-scenario-coverage=false
|
|
33
|
+
npm run dev -- report --specs "examples/**/*.feature.md" --tests "tests/**/*.test.ts"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Library API
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import {
|
|
40
|
+
checkFeatureSpecs,
|
|
41
|
+
parseFeatureSpec,
|
|
42
|
+
renderHtmlReport,
|
|
43
|
+
validateFeatureSpec,
|
|
44
|
+
} from "feature-spec-md";
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Development
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install
|
|
51
|
+
npm test
|
|
52
|
+
npm run typecheck
|
|
53
|
+
npm run build
|
|
54
|
+
```
|
package/SPEC_FORMAT.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Feature Spec Markdown format
|
|
2
|
+
|
|
3
|
+
Feature Spec Markdown is a lightweight convention for readable, testable feature specifications.
|
|
4
|
+
|
|
5
|
+
It intentionally uses ordinary Markdown and stable IDs instead of a dedicated executable specification language.
|
|
6
|
+
|
|
7
|
+
## File name
|
|
8
|
+
|
|
9
|
+
Use:
|
|
10
|
+
|
|
11
|
+
```txt
|
|
12
|
+
*.feature.md
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
|
|
17
|
+
```txt
|
|
18
|
+
account-access.feature.md
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Frontmatter
|
|
22
|
+
|
|
23
|
+
Every file starts with YAML-like frontmatter:
|
|
24
|
+
|
|
25
|
+
```md
|
|
26
|
+
---
|
|
27
|
+
id: ACCOUNT
|
|
28
|
+
title: Account access
|
|
29
|
+
status: draft
|
|
30
|
+
---
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Required fields:
|
|
34
|
+
|
|
35
|
+
- `id`
|
|
36
|
+
- `title`
|
|
37
|
+
|
|
38
|
+
Optional fields:
|
|
39
|
+
|
|
40
|
+
- `status`: `draft`, `active`, or `deprecated`
|
|
41
|
+
- `owner`
|
|
42
|
+
|
|
43
|
+
## Required sections
|
|
44
|
+
|
|
45
|
+
```md
|
|
46
|
+
# Account access
|
|
47
|
+
|
|
48
|
+
## Purpose
|
|
49
|
+
|
|
50
|
+
## Rules
|
|
51
|
+
|
|
52
|
+
## Scenarios
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Rules
|
|
56
|
+
|
|
57
|
+
Rules are durable business truths. They use stable IDs and requirement keywords.
|
|
58
|
+
|
|
59
|
+
```md
|
|
60
|
+
- ACCOUNT-R001: A person MUST prove control of a registered email address before accessing an account.
|
|
61
|
+
- ACCOUNT-R002: The system MUST NOT reveal whether an unknown email address belongs to an account.
|
|
62
|
+
- ACCOUNT-R003: A signed-in person SHOULD be returned to the page they originally requested.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use these keywords:
|
|
66
|
+
|
|
67
|
+
- `MUST`
|
|
68
|
+
- `MUST NOT`
|
|
69
|
+
- `SHOULD`
|
|
70
|
+
- `SHOULD NOT`
|
|
71
|
+
- `MAY`
|
|
72
|
+
- `OPTIONAL`
|
|
73
|
+
|
|
74
|
+
## Scenarios
|
|
75
|
+
|
|
76
|
+
Scenarios are concrete examples. They use stable IDs and Given / When / Then steps.
|
|
77
|
+
|
|
78
|
+
```md
|
|
79
|
+
### ACCOUNT-S001: Registered person signs in
|
|
80
|
+
|
|
81
|
+
Given a registered person is on the sign-in page
|
|
82
|
+
When they request and open a valid sign-in link
|
|
83
|
+
Then they are signed in
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Allowed step keywords:
|
|
87
|
+
|
|
88
|
+
- `Given`
|
|
89
|
+
- `When`
|
|
90
|
+
- `Then`
|
|
91
|
+
- `And`
|
|
92
|
+
- `But`
|
|
93
|
+
|
|
94
|
+
## Test coverage convention
|
|
95
|
+
|
|
96
|
+
The spec file does not contain test mappings.
|
|
97
|
+
|
|
98
|
+
Tests reference rule and scenario IDs in test titles, tags, annotations, comments, or metadata.
|
|
99
|
+
|
|
100
|
+
Generated tooling can then answer:
|
|
101
|
+
|
|
102
|
+
- Which scenarios have tests?
|
|
103
|
+
- Which rules have executable coverage?
|
|
104
|
+
- Which tests reference deleted or unknown spec IDs?
|
|
105
|
+
- Which visible flows have screenshots, traces, or other evidence?
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { checkFeatureSpecs, renderHtmlReport, writeTextFile } from "./index.js";
|
|
5
|
+
const defaultSpecPattern = "specs/**/*.feature.md";
|
|
6
|
+
const defaultTestPattern = "tests/**/*.spec.ts";
|
|
7
|
+
main().catch((error) => {
|
|
8
|
+
console.error(error instanceof Error ? error.message : error);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
async function main() {
|
|
12
|
+
const [command = "help", ...args] = process.argv.slice(2);
|
|
13
|
+
const options = parseArgs(args);
|
|
14
|
+
if (command === "check")
|
|
15
|
+
return runCheck(options);
|
|
16
|
+
if (command === "report")
|
|
17
|
+
return runReport(options);
|
|
18
|
+
if (command === "init")
|
|
19
|
+
return runInit(options);
|
|
20
|
+
printHelp();
|
|
21
|
+
process.exit(command === "help" || command === "--help" || command === "-h" ? 0 : 1);
|
|
22
|
+
}
|
|
23
|
+
async function runCheck(options) {
|
|
24
|
+
const result = await checkFeatureSpecs({
|
|
25
|
+
specs: optionList(options.specs, defaultSpecPattern),
|
|
26
|
+
tests: options.tests === "" ? [] : optionList(options.tests, defaultTestPattern),
|
|
27
|
+
requireRuleCoverage: options["require-rule-coverage"] === "true",
|
|
28
|
+
requireScenarioCoverage: options["require-scenario-coverage"] !== "false",
|
|
29
|
+
});
|
|
30
|
+
printIssues([...result.validationIssues, ...result.coverageIssues]);
|
|
31
|
+
if (!result.ok)
|
|
32
|
+
process.exit(1);
|
|
33
|
+
const scenarioCount = result.specs.reduce((sum, spec) => sum + spec.scenarios.length, 0);
|
|
34
|
+
const ruleCount = result.specs.reduce((sum, spec) => sum + spec.rules.length, 0);
|
|
35
|
+
console.log(`Feature spec check passed: ${result.specs.length} spec(s), ${ruleCount} rule(s), ${scenarioCount} scenario(s).`);
|
|
36
|
+
}
|
|
37
|
+
async function runReport(options) {
|
|
38
|
+
const result = await checkFeatureSpecs({
|
|
39
|
+
specs: optionList(options.specs, defaultSpecPattern),
|
|
40
|
+
tests: options.tests === "" ? [] : optionList(options.tests, defaultTestPattern),
|
|
41
|
+
requireRuleCoverage: options["require-rule-coverage"] === "true",
|
|
42
|
+
requireScenarioCoverage: false,
|
|
43
|
+
});
|
|
44
|
+
const out = options.out ?? "test-results/feature-spec-report/index.html";
|
|
45
|
+
await writeTextFile(out, renderHtmlReport(result.specs, {
|
|
46
|
+
coverage: result.coverage,
|
|
47
|
+
validationIssues: [...result.validationIssues, ...result.coverageIssues],
|
|
48
|
+
}));
|
|
49
|
+
console.log(`Feature spec report written to ${out}`);
|
|
50
|
+
}
|
|
51
|
+
async function runInit(options) {
|
|
52
|
+
const dir = options.dir ?? "specs";
|
|
53
|
+
const target = path.join(dir, "account-access.feature.md");
|
|
54
|
+
await mkdir(dir, { recursive: true });
|
|
55
|
+
await writeFile(target, exampleSpec, "utf8");
|
|
56
|
+
console.log(`Created ${target}`);
|
|
57
|
+
}
|
|
58
|
+
function parseArgs(args) {
|
|
59
|
+
const options = {};
|
|
60
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
61
|
+
const arg = args[index];
|
|
62
|
+
if (!arg.startsWith("--"))
|
|
63
|
+
continue;
|
|
64
|
+
const [key, inlineValue] = arg.slice(2).split("=", 2);
|
|
65
|
+
if (inlineValue !== undefined) {
|
|
66
|
+
options[key] = inlineValue;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const next = args[index + 1];
|
|
70
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
71
|
+
options[key] = next;
|
|
72
|
+
index += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
options[key] = "true";
|
|
76
|
+
}
|
|
77
|
+
return options;
|
|
78
|
+
}
|
|
79
|
+
function optionList(value, fallback) {
|
|
80
|
+
return (value ?? fallback).split(",").map((item) => item.trim()).filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
function printIssues(issues) {
|
|
83
|
+
for (const issue of issues) {
|
|
84
|
+
const location = issue.filePath ? `${issue.filePath}${issue.line ? `:${issue.line}` : ""}` : "";
|
|
85
|
+
console.error(`${issue.severity.toUpperCase()} ${issue.code}${location ? ` ${location}` : ""}: ${issue.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function printHelp() {
|
|
89
|
+
console.log(`feature-spec-md
|
|
90
|
+
|
|
91
|
+
Usage:
|
|
92
|
+
feature-spec-md init [--dir specs]
|
|
93
|
+
feature-spec-md check [--specs "specs/**/*.feature.md"] [--tests "tests/**/*.spec.ts"]
|
|
94
|
+
feature-spec-md report [--specs "specs/**/*.feature.md"] [--tests "tests/**/*.spec.ts"] [--out test-results/feature-spec-report/index.html]
|
|
95
|
+
|
|
96
|
+
Options:
|
|
97
|
+
--require-rule-coverage Fail when rules have no matching test references.
|
|
98
|
+
--require-scenario-coverage Defaults to true for check. Use --require-scenario-coverage=false to disable.
|
|
99
|
+
--tests "" Disable test coverage lookup.
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
102
|
+
const exampleSpec = `---
|
|
103
|
+
id: ACCOUNT
|
|
104
|
+
title: Account access
|
|
105
|
+
status: draft
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
# Account access
|
|
109
|
+
|
|
110
|
+
## Purpose
|
|
111
|
+
|
|
112
|
+
People can access their own account after proving their identity.
|
|
113
|
+
|
|
114
|
+
## Rules
|
|
115
|
+
|
|
116
|
+
- ACCOUNT-R001: A person MUST prove control of a registered email address before accessing an account.
|
|
117
|
+
- ACCOUNT-R002: The system MUST NOT reveal whether an unknown email address belongs to an account.
|
|
118
|
+
- ACCOUNT-R003: A signed-in person SHOULD be returned to the page they originally requested.
|
|
119
|
+
|
|
120
|
+
## Scenarios
|
|
121
|
+
|
|
122
|
+
### ACCOUNT-S001: Registered person signs in
|
|
123
|
+
|
|
124
|
+
Given a registered person is on the sign-in page
|
|
125
|
+
When they request and open a valid sign-in link
|
|
126
|
+
Then they are signed in
|
|
127
|
+
And they are returned to the page they originally requested
|
|
128
|
+
|
|
129
|
+
### ACCOUNT-S002: Unknown email receives a neutral response
|
|
130
|
+
|
|
131
|
+
Given a person enters an email address that is not registered
|
|
132
|
+
When they request a sign-in link
|
|
133
|
+
Then the response says to check their email
|
|
134
|
+
And the response does not reveal whether the email address is registered
|
|
135
|
+
`;
|
|
136
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type FeatureSpec = {
|
|
2
|
+
filePath: string;
|
|
3
|
+
frontmatter: FeatureFrontmatter;
|
|
4
|
+
title: string;
|
|
5
|
+
purpose: string;
|
|
6
|
+
rules: FeatureRule[];
|
|
7
|
+
scenarios: FeatureScenario[];
|
|
8
|
+
source: string;
|
|
9
|
+
};
|
|
10
|
+
export type FeatureFrontmatter = {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
status?: "draft" | "active" | "deprecated";
|
|
14
|
+
owner?: string;
|
|
15
|
+
};
|
|
16
|
+
export type FeatureRule = {
|
|
17
|
+
id: string;
|
|
18
|
+
text: string;
|
|
19
|
+
keyword?: RuleKeyword;
|
|
20
|
+
strength: "required" | "recommended" | "optional" | "unspecified";
|
|
21
|
+
line: number;
|
|
22
|
+
};
|
|
23
|
+
export type FeatureScenario = {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
line: number;
|
|
27
|
+
steps: Array<{
|
|
28
|
+
keyword: StepKeyword;
|
|
29
|
+
text: string;
|
|
30
|
+
line: number;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
export type RuleKeyword = "MUST" | "MUST NOT" | "SHOULD" | "SHOULD NOT" | "MAY" | "OPTIONAL";
|
|
34
|
+
export type StepKeyword = "Given" | "When" | "Then" | "And" | "But";
|
|
35
|
+
export type ValidationIssue = {
|
|
36
|
+
code: string;
|
|
37
|
+
severity: "error" | "warning";
|
|
38
|
+
message: string;
|
|
39
|
+
filePath?: string;
|
|
40
|
+
line?: number;
|
|
41
|
+
};
|
|
42
|
+
export type TestReference = {
|
|
43
|
+
id: string;
|
|
44
|
+
filePath: string;
|
|
45
|
+
line: number;
|
|
46
|
+
kind: "scenario" | "rule";
|
|
47
|
+
source: "title" | "tag" | "covers" | "annotation" | "free-text";
|
|
48
|
+
};
|
|
49
|
+
export type CoverageItem = {
|
|
50
|
+
id: string;
|
|
51
|
+
title?: string;
|
|
52
|
+
filePath?: string;
|
|
53
|
+
line?: number;
|
|
54
|
+
covered: boolean;
|
|
55
|
+
references: TestReference[];
|
|
56
|
+
};
|
|
57
|
+
export type CoverageSummary = {
|
|
58
|
+
scenarioCoverage: CoverageItem[];
|
|
59
|
+
ruleCoverage: CoverageItem[];
|
|
60
|
+
orphanScenarioReferences: TestReference[];
|
|
61
|
+
orphanRuleReferences: TestReference[];
|
|
62
|
+
};
|
|
63
|
+
export declare function parseFeatureSpec(source: string, options?: {
|
|
64
|
+
filePath?: string;
|
|
65
|
+
}): FeatureSpec;
|
|
66
|
+
export declare function validateFeatureSpec(spec: FeatureSpec): ValidationIssue[];
|
|
67
|
+
export declare function parseTestReferences(source: string, filePath?: string): TestReference[];
|
|
68
|
+
export declare function collectTestReferences(patterns: string[]): Promise<TestReference[]>;
|
|
69
|
+
export declare function buildCoverageSummary(specs: FeatureSpec[], references: TestReference[]): CoverageSummary;
|
|
70
|
+
export declare function validateCoverage(coverage: CoverageSummary, options?: {
|
|
71
|
+
requireRuleCoverage?: boolean;
|
|
72
|
+
requireScenarioCoverage?: boolean;
|
|
73
|
+
}): ValidationIssue[];
|
|
74
|
+
export declare function loadFeatureSpecs(patterns: string[]): Promise<FeatureSpec[]>;
|
|
75
|
+
export declare function checkFeatureSpecs(options: {
|
|
76
|
+
specs: string[];
|
|
77
|
+
tests?: string[];
|
|
78
|
+
requireRuleCoverage?: boolean;
|
|
79
|
+
requireScenarioCoverage?: boolean;
|
|
80
|
+
}): Promise<{
|
|
81
|
+
specs: FeatureSpec[];
|
|
82
|
+
validationIssues: ValidationIssue[];
|
|
83
|
+
coverage: CoverageSummary | undefined;
|
|
84
|
+
coverageIssues: ValidationIssue[];
|
|
85
|
+
ok: boolean;
|
|
86
|
+
}>;
|
|
87
|
+
export declare function renderHtmlReport(specs: FeatureSpec[], options?: {
|
|
88
|
+
coverage?: CoverageSummary;
|
|
89
|
+
validationIssues?: ValidationIssue[];
|
|
90
|
+
title?: string;
|
|
91
|
+
generatedAt?: string;
|
|
92
|
+
}): string;
|
|
93
|
+
export declare function writeTextFile(filePath: string, content: string): Promise<void>;
|
|
94
|
+
export declare function expandFilePatterns(patterns: string[]): Promise<string[]>;
|
|
95
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { glob, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const scenarioIdPattern = /\b[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-S\d{3}\b/g;
|
|
4
|
+
const ruleIdPattern = /\b[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-R\d{3}\b/g;
|
|
5
|
+
const ruleKeywords = ["MUST NOT", "SHOULD NOT", "MUST", "SHOULD", "MAY", "OPTIONAL"];
|
|
6
|
+
export function parseFeatureSpec(source, options = {}) {
|
|
7
|
+
const filePath = options.filePath ?? "<inline>";
|
|
8
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
9
|
+
if (!normalized.startsWith("---\n")) {
|
|
10
|
+
throw new Error("Feature spec must start with frontmatter delimited by ---. ");
|
|
11
|
+
}
|
|
12
|
+
const endIndex = normalized.indexOf("\n---\n", 4);
|
|
13
|
+
if (endIndex === -1) {
|
|
14
|
+
throw new Error("Feature spec frontmatter must end with a second --- delimiter.");
|
|
15
|
+
}
|
|
16
|
+
const frontmatter = parseFrontmatter(normalized.slice(4, endIndex));
|
|
17
|
+
if (!frontmatter.id || !frontmatter.title) {
|
|
18
|
+
throw new Error("Feature spec frontmatter must contain id and title.");
|
|
19
|
+
}
|
|
20
|
+
const body = normalized.slice(endIndex + 5);
|
|
21
|
+
const bodyStartLine = normalized.slice(0, endIndex + 5).split("\n").length;
|
|
22
|
+
const lines = body.split("\n");
|
|
23
|
+
return {
|
|
24
|
+
filePath,
|
|
25
|
+
frontmatter: frontmatter,
|
|
26
|
+
title: lines.find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim() ?? frontmatter.title,
|
|
27
|
+
purpose: sectionText(lines, "Purpose").trim(),
|
|
28
|
+
rules: parseRules(lines, bodyStartLine),
|
|
29
|
+
scenarios: parseScenarios(lines, bodyStartLine),
|
|
30
|
+
source,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function validateFeatureSpec(spec) {
|
|
34
|
+
const issues = [];
|
|
35
|
+
const seen = new Map();
|
|
36
|
+
if (!/^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*$/.test(spec.frontmatter.id)) {
|
|
37
|
+
issues.push(issue(spec, "invalid-feature-id", "error", `Feature id "${spec.frontmatter.id}" must use uppercase words separated by hyphens.`));
|
|
38
|
+
}
|
|
39
|
+
if (spec.title !== spec.frontmatter.title) {
|
|
40
|
+
issues.push(issue(spec, "title-mismatch", "warning", "The H1 title should match frontmatter.title."));
|
|
41
|
+
}
|
|
42
|
+
if (!spec.purpose) {
|
|
43
|
+
issues.push(issue(spec, "missing-purpose", "error", "Feature spec must include a non-empty ## Purpose section."));
|
|
44
|
+
}
|
|
45
|
+
if (spec.scenarios.length === 0) {
|
|
46
|
+
issues.push(issue(spec, "missing-scenarios", "error", "Feature spec must include at least one scenario."));
|
|
47
|
+
}
|
|
48
|
+
for (const rule of spec.rules) {
|
|
49
|
+
if (!rule.id.startsWith(`${spec.frontmatter.id}-`)) {
|
|
50
|
+
issues.push(issue(spec, "wrong-rule-prefix", "error", `Rule id "${rule.id}" must start with ${spec.frontmatter.id}-.`, rule.line));
|
|
51
|
+
}
|
|
52
|
+
if (!rule.keyword) {
|
|
53
|
+
issues.push(issue(spec, "missing-rule-keyword", "warning", `Rule "${rule.id}" should use MUST, MUST NOT, SHOULD, SHOULD NOT, MAY, or OPTIONAL.`, rule.line));
|
|
54
|
+
}
|
|
55
|
+
registerId(spec, issues, seen, rule.id, rule.line);
|
|
56
|
+
}
|
|
57
|
+
for (const scenario of spec.scenarios) {
|
|
58
|
+
if (!scenario.id.startsWith(`${spec.frontmatter.id}-`)) {
|
|
59
|
+
issues.push(issue(spec, "wrong-scenario-prefix", "error", `Scenario id "${scenario.id}" must start with ${spec.frontmatter.id}-.`, scenario.line));
|
|
60
|
+
}
|
|
61
|
+
for (const keyword of ["Given", "When", "Then"]) {
|
|
62
|
+
if (!scenario.steps.some((step) => step.keyword === keyword)) {
|
|
63
|
+
issues.push(issue(spec, `missing-${keyword.toLowerCase()}`, "warning", `Scenario "${scenario.id}" should include a ${keyword} step.`, scenario.line));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
registerId(spec, issues, seen, scenario.id, scenario.line);
|
|
67
|
+
}
|
|
68
|
+
return issues;
|
|
69
|
+
}
|
|
70
|
+
export function parseTestReferences(source, filePath = "<inline>") {
|
|
71
|
+
const lines = source.split(/\r?\n/);
|
|
72
|
+
const refs = [];
|
|
73
|
+
for (const match of source.matchAll(scenarioIdPattern)) {
|
|
74
|
+
refs.push({
|
|
75
|
+
id: match[0],
|
|
76
|
+
filePath,
|
|
77
|
+
line: lineForOffset(lines, match.index ?? 0),
|
|
78
|
+
kind: "scenario",
|
|
79
|
+
source: sourceForMatch(source, match.index ?? 0),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const match of source.matchAll(ruleIdPattern)) {
|
|
83
|
+
refs.push({
|
|
84
|
+
id: match[0],
|
|
85
|
+
filePath,
|
|
86
|
+
line: lineForOffset(lines, match.index ?? 0),
|
|
87
|
+
kind: "rule",
|
|
88
|
+
source: sourceForMatch(source, match.index ?? 0),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return dedupe(refs);
|
|
92
|
+
}
|
|
93
|
+
export async function collectTestReferences(patterns) {
|
|
94
|
+
const refs = [];
|
|
95
|
+
for (const file of await expandFilePatterns(patterns)) {
|
|
96
|
+
refs.push(...parseTestReferences(await readFile(file, "utf8"), file));
|
|
97
|
+
}
|
|
98
|
+
return refs;
|
|
99
|
+
}
|
|
100
|
+
export function buildCoverageSummary(specs, references) {
|
|
101
|
+
const scenarios = new Map(specs.flatMap((spec) => spec.scenarios.map((scenario) => [scenario.id, { spec, scenario }])));
|
|
102
|
+
const rules = new Map(specs.flatMap((spec) => spec.rules.map((rule) => [rule.id, { spec, rule }])));
|
|
103
|
+
const scenarioRefs = references.filter((ref) => ref.kind === "scenario");
|
|
104
|
+
const ruleRefs = references.filter((ref) => ref.kind === "rule");
|
|
105
|
+
return {
|
|
106
|
+
scenarioCoverage: Array.from(scenarios.entries()).map(([id, value]) => item(id, value.scenario.title, value.spec.filePath, value.scenario.line, scenarioRefs)),
|
|
107
|
+
ruleCoverage: Array.from(rules.entries()).map(([id, value]) => item(id, value.rule.text, value.spec.filePath, value.rule.line, ruleRefs)),
|
|
108
|
+
orphanScenarioReferences: scenarioRefs.filter((ref) => !scenarios.has(ref.id)),
|
|
109
|
+
orphanRuleReferences: ruleRefs.filter((ref) => !rules.has(ref.id)),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function validateCoverage(coverage, options = {}) {
|
|
113
|
+
const issues = [];
|
|
114
|
+
if (options.requireScenarioCoverage ?? true) {
|
|
115
|
+
for (const item of coverage.scenarioCoverage.filter((item) => !item.covered)) {
|
|
116
|
+
issues.push({ code: "missing-scenario-coverage", severity: "error", message: `Scenario "${item.id}" has no matching test reference.`, filePath: item.filePath, line: item.line });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (options.requireRuleCoverage ?? false) {
|
|
120
|
+
for (const item of coverage.ruleCoverage.filter((item) => !item.covered)) {
|
|
121
|
+
issues.push({ code: "missing-rule-coverage", severity: "error", message: `Rule "${item.id}" has no matching test reference.`, filePath: item.filePath, line: item.line });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const ref of coverage.orphanScenarioReferences) {
|
|
125
|
+
issues.push({ code: "orphan-scenario-reference", severity: "error", message: `Test references unknown scenario "${ref.id}".`, filePath: ref.filePath, line: ref.line });
|
|
126
|
+
}
|
|
127
|
+
for (const ref of coverage.orphanRuleReferences) {
|
|
128
|
+
issues.push({ code: "orphan-rule-reference", severity: "error", message: `Test references unknown rule "${ref.id}".`, filePath: ref.filePath, line: ref.line });
|
|
129
|
+
}
|
|
130
|
+
return issues;
|
|
131
|
+
}
|
|
132
|
+
export async function loadFeatureSpecs(patterns) {
|
|
133
|
+
const specs = [];
|
|
134
|
+
for (const file of await expandFilePatterns(patterns)) {
|
|
135
|
+
specs.push(parseFeatureSpec(await readFile(file, "utf8"), { filePath: file }));
|
|
136
|
+
}
|
|
137
|
+
return specs;
|
|
138
|
+
}
|
|
139
|
+
export async function checkFeatureSpecs(options) {
|
|
140
|
+
const specs = await loadFeatureSpecs(options.specs);
|
|
141
|
+
const validationIssues = specs.flatMap(validateFeatureSpec);
|
|
142
|
+
const coverage = options.tests?.length ? buildCoverageSummary(specs, await collectTestReferences(options.tests)) : undefined;
|
|
143
|
+
const coverageIssues = coverage ? validateCoverage(coverage, { requireRuleCoverage: options.requireRuleCoverage, requireScenarioCoverage: options.requireScenarioCoverage }) : [];
|
|
144
|
+
return {
|
|
145
|
+
specs,
|
|
146
|
+
validationIssues,
|
|
147
|
+
coverage,
|
|
148
|
+
coverageIssues,
|
|
149
|
+
ok: ![...validationIssues, ...coverageIssues].some((i) => i.severity === "error"),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export function renderHtmlReport(specs, options = {}) {
|
|
153
|
+
const title = options.title ?? "Feature Spec Report";
|
|
154
|
+
const issues = options.validationIssues ?? [];
|
|
155
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${html(title)}</title><style>body{font-family:system-ui,sans-serif;max-width:1100px;margin:0 auto;padding:40px 24px}.panel{border:1px solid #ddd;border-radius:16px;padding:20px;margin:18px 0}.ok{color:#1a7f37}.missing,.error{color:#cf222e}.warning{color:#9a6700}.badge{border:1px solid #ddd;border-radius:999px;padding:2px 8px;font-size:12px}</style></head><body><h1>${html(title)}</h1><p>Generated ${html(options.generatedAt ?? new Date().toISOString())}.</p>${renderIssues(issues)}${specs.map((spec) => renderSpec(spec, options.coverage)).join("")}</body></html>`;
|
|
156
|
+
}
|
|
157
|
+
export async function writeTextFile(filePath, content) {
|
|
158
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
159
|
+
await writeFile(filePath, content, "utf8");
|
|
160
|
+
}
|
|
161
|
+
export async function expandFilePatterns(patterns) {
|
|
162
|
+
const files = new Set();
|
|
163
|
+
for (const pattern of patterns) {
|
|
164
|
+
for await (const file of glob(pattern, { exclude: ["node_modules/**", ".git/**", "dist/**", "test-results/**"] })) {
|
|
165
|
+
files.add(file);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return Array.from(files).sort();
|
|
169
|
+
}
|
|
170
|
+
function parseFrontmatter(source) {
|
|
171
|
+
const data = {};
|
|
172
|
+
for (const line of source.split("\n")) {
|
|
173
|
+
const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
174
|
+
if (match)
|
|
175
|
+
data[match[1]] = match[2].replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "").trim();
|
|
176
|
+
}
|
|
177
|
+
return data;
|
|
178
|
+
}
|
|
179
|
+
function sectionBounds(lines, heading) {
|
|
180
|
+
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
181
|
+
if (start === -1)
|
|
182
|
+
return null;
|
|
183
|
+
let end = lines.length;
|
|
184
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
185
|
+
if (/^##\s+/.test(lines[i])) {
|
|
186
|
+
end = i;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { start: start + 1, end };
|
|
191
|
+
}
|
|
192
|
+
function sectionText(lines, heading) {
|
|
193
|
+
const bounds = sectionBounds(lines, heading);
|
|
194
|
+
return bounds ? lines.slice(bounds.start, bounds.end).join("\n") : "";
|
|
195
|
+
}
|
|
196
|
+
function parseRules(lines, bodyStartLine) {
|
|
197
|
+
const bounds = sectionBounds(lines, "Rules");
|
|
198
|
+
if (!bounds)
|
|
199
|
+
return [];
|
|
200
|
+
const rules = [];
|
|
201
|
+
for (let i = bounds.start; i < bounds.end; i += 1) {
|
|
202
|
+
const match = lines[i].match(/^\s*-\s+([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-R\d{3}):\s+(.+)$/);
|
|
203
|
+
if (!match)
|
|
204
|
+
continue;
|
|
205
|
+
const keyword = ruleKeywords.find((kw) => match[2].toUpperCase().includes(kw));
|
|
206
|
+
rules.push({ id: match[1], text: match[2].trim(), keyword, strength: strength(keyword), line: bodyStartLine + i });
|
|
207
|
+
}
|
|
208
|
+
return rules;
|
|
209
|
+
}
|
|
210
|
+
function parseScenarios(lines, bodyStartLine) {
|
|
211
|
+
const scenarios = [];
|
|
212
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
213
|
+
const match = lines[i].match(/^###\s+([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-S\d{3}):\s+(.+)$/);
|
|
214
|
+
if (!match)
|
|
215
|
+
continue;
|
|
216
|
+
const steps = [];
|
|
217
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
218
|
+
if (/^##?#\s+/.test(lines[j]))
|
|
219
|
+
break;
|
|
220
|
+
const step = lines[j].trim().match(/^(Given|When|Then|And|But)\s+(.+)$/);
|
|
221
|
+
if (step)
|
|
222
|
+
steps.push({ keyword: step[1], text: step[2].trim(), line: bodyStartLine + j });
|
|
223
|
+
}
|
|
224
|
+
scenarios.push({ id: match[1], title: match[2].trim(), line: bodyStartLine + i, steps });
|
|
225
|
+
}
|
|
226
|
+
return scenarios;
|
|
227
|
+
}
|
|
228
|
+
function strength(keyword) {
|
|
229
|
+
if (keyword === "MUST" || keyword === "MUST NOT")
|
|
230
|
+
return "required";
|
|
231
|
+
if (keyword === "SHOULD" || keyword === "SHOULD NOT")
|
|
232
|
+
return "recommended";
|
|
233
|
+
if (keyword === "MAY" || keyword === "OPTIONAL")
|
|
234
|
+
return "optional";
|
|
235
|
+
return "unspecified";
|
|
236
|
+
}
|
|
237
|
+
function issue(spec, code, severity, message, line) {
|
|
238
|
+
return { code, severity, message, filePath: spec.filePath, line };
|
|
239
|
+
}
|
|
240
|
+
function registerId(spec, issues, seen, id, line) {
|
|
241
|
+
const previous = seen.get(id);
|
|
242
|
+
if (previous)
|
|
243
|
+
issues.push(issue(spec, "duplicate-id", "error", `Duplicate id "${id}". First occurrence is on line ${previous}.`, line));
|
|
244
|
+
seen.set(id, line);
|
|
245
|
+
}
|
|
246
|
+
function item(id, title, filePath, line, refs) {
|
|
247
|
+
const references = refs.filter((ref) => ref.id === id);
|
|
248
|
+
return { id, title, filePath, line, references, covered: references.length > 0 };
|
|
249
|
+
}
|
|
250
|
+
function lineForOffset(lines, offset) {
|
|
251
|
+
let consumed = 0;
|
|
252
|
+
for (const [index, line] of lines.entries()) {
|
|
253
|
+
consumed += line.length + 1;
|
|
254
|
+
if (consumed > offset)
|
|
255
|
+
return index + 1;
|
|
256
|
+
}
|
|
257
|
+
return lines.length;
|
|
258
|
+
}
|
|
259
|
+
function sourceForMatch(source, offset) {
|
|
260
|
+
const before = source.slice(Math.max(0, offset - 80), offset);
|
|
261
|
+
const context = source.slice(Math.max(0, offset - 80), offset + 80);
|
|
262
|
+
if (/covers\s*:\s*\[[^\]]*$/.test(before))
|
|
263
|
+
return "covers";
|
|
264
|
+
if (/tag\s*:\s*\[[^\]]*$/.test(before) || /@/.test(source.slice(Math.max(0, offset - 2), offset + 2)))
|
|
265
|
+
return "tag";
|
|
266
|
+
if (/annotation/.test(context))
|
|
267
|
+
return "annotation";
|
|
268
|
+
if (/test|scenario/.test(before.slice(-40)))
|
|
269
|
+
return "title";
|
|
270
|
+
return "free-text";
|
|
271
|
+
}
|
|
272
|
+
function dedupe(refs) {
|
|
273
|
+
const seen = new Set();
|
|
274
|
+
return refs.filter((ref) => {
|
|
275
|
+
const key = `${ref.kind}:${ref.id}:${ref.filePath}:${ref.line}:${ref.source}`;
|
|
276
|
+
if (seen.has(key))
|
|
277
|
+
return false;
|
|
278
|
+
seen.add(key);
|
|
279
|
+
return true;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function renderIssues(issues) {
|
|
283
|
+
if (!issues.length)
|
|
284
|
+
return `<section class="panel"><h2>Validation</h2><p class="ok">No validation issues found.</p></section>`;
|
|
285
|
+
return `<section class="panel"><h2>Validation</h2><ul>${issues.map((i) => `<li class="${i.severity}"><code>${html(`${i.filePath ?? ""}${i.line ? `:${i.line}` : ""}`)}</code> ${html(i.message)}</li>`).join("")}</ul></section>`;
|
|
286
|
+
}
|
|
287
|
+
function renderSpec(spec, coverage) {
|
|
288
|
+
return `<section class="panel"><p><span class="badge">${html(spec.frontmatter.status ?? "draft")}</span></p><h2>${html(spec.frontmatter.id)} ${html(spec.title)}</h2><p>${html(spec.purpose)}</p><h3>Rules</h3><ul>${spec.rules.map((r) => `<li><code>${html(r.id)}</code>: ${html(r.text)} ${coverageBadge(coverage?.ruleCoverage.find((i) => i.id === r.id)?.covered)}</li>`).join("")}</ul><h3>Scenarios</h3>${spec.scenarios.map((s) => `<article class="panel"><h4><code>${html(s.id)}</code>: ${html(s.title)} ${coverageBadge(coverage?.scenarioCoverage.find((i) => i.id === s.id)?.covered)}</h4>${s.steps.map((step) => `<p><strong>${html(step.keyword)}</strong> ${html(step.text)}</p>`).join("")}</article>`).join("")}</section>`;
|
|
289
|
+
}
|
|
290
|
+
function coverageBadge(covered) {
|
|
291
|
+
return covered === undefined ? "" : covered ? `<span class="badge ok">covered</span>` : `<span class="badge missing">missing coverage</span>`;
|
|
292
|
+
}
|
|
293
|
+
function html(value) {
|
|
294
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: ACCOUNT
|
|
3
|
+
title: Account access
|
|
4
|
+
status: draft
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Account access
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
People can access their account after completing the required flow.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
- ACCOUNT-R001: A person MUST complete the required flow before account access is granted.
|
|
16
|
+
- ACCOUNT-R002: The system MUST NOT expose internal details in user-facing messages.
|
|
17
|
+
- ACCOUNT-R003: A returning person SHOULD be sent back to the page they originally requested.
|
|
18
|
+
|
|
19
|
+
## Scenarios
|
|
20
|
+
|
|
21
|
+
### ACCOUNT-S001: Returning person completes access flow
|
|
22
|
+
|
|
23
|
+
Given a returning person is on the access page
|
|
24
|
+
When they complete the required flow
|
|
25
|
+
Then account access is granted
|
|
26
|
+
And they are sent back to the page they originally requested
|
|
27
|
+
|
|
28
|
+
### ACCOUNT-S002: Unknown input receives a neutral response
|
|
29
|
+
|
|
30
|
+
Given a person enters input that does not match a known account
|
|
31
|
+
When they request access
|
|
32
|
+
Then the response is neutral
|
|
33
|
+
And the response does not expose internal details
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anselmdk/feature-spec-md",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Markdown feature specs with rule/scenario IDs, validation, coverage checks, and generated reports.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"feature-spec-md": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"SPEC_FORMAT.md",
|
|
19
|
+
"templates",
|
|
20
|
+
"examples",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"prepare": "npm run build",
|
|
29
|
+
"dev": "tsx src/cli.ts",
|
|
30
|
+
"check:example": "tsx src/cli.ts check --specs examples/**/*.feature.md --tests tests/**/*.test.ts --require-scenario-coverage=false",
|
|
31
|
+
"report:example": "tsx src/cli.ts report --specs examples/**/*.feature.md --tests tests/**/*.test.ts --out test-results/feature-spec-report/index.html",
|
|
32
|
+
"test": "tsx --test tests/**/*.test.ts",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"verify": "npm run typecheck && npm test && npm run build && npm run check:example"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"markdown",
|
|
38
|
+
"bdd",
|
|
39
|
+
"gherkin",
|
|
40
|
+
"playwright",
|
|
41
|
+
"specification",
|
|
42
|
+
"testing"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^25.3.2",
|
|
47
|
+
"tsx": "^4.21.0",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: FEATURE
|
|
3
|
+
title: Feature title
|
|
4
|
+
status: draft
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Feature title
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Describe the outcome this feature provides.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
- FEATURE-R001: The system MUST enforce an important rule.
|
|
16
|
+
- FEATURE-R002: The system SHOULD provide a helpful default.
|
|
17
|
+
|
|
18
|
+
## Scenarios
|
|
19
|
+
|
|
20
|
+
### FEATURE-S001: Main path
|
|
21
|
+
|
|
22
|
+
Given a precondition is true
|
|
23
|
+
When a person performs an action
|
|
24
|
+
Then the expected outcome is visible
|