@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 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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;").replace(/'/g, "&#039;");
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