@anselmdk/feature-spec-md 0.3.1 → 0.5.0-rc.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.
Files changed (41) hide show
  1. package/README.md +170 -39
  2. package/SPEC_FORMAT.md +38 -78
  3. package/dist/cli.js +98 -81
  4. package/dist/cli.js.map +1 -1
  5. package/dist/featureSpecs.d.ts +1 -1
  6. package/dist/featureSpecs.d.ts.map +1 -1
  7. package/dist/featureSpecs.js +57 -10
  8. package/dist/featureSpecs.js.map +1 -1
  9. package/dist/githubActionDiffReport.d.ts +4 -0
  10. package/dist/githubActionDiffReport.d.ts.map +1 -0
  11. package/dist/githubActionDiffReport.js +209 -0
  12. package/dist/githubActionDiffReport.js.map +1 -0
  13. package/dist/githubActionFtp.d.ts +25 -0
  14. package/dist/githubActionFtp.d.ts.map +1 -0
  15. package/dist/githubActionFtp.js +210 -0
  16. package/dist/githubActionFtp.js.map +1 -0
  17. package/dist/githubActionOutput.d.ts +3 -0
  18. package/dist/githubActionOutput.d.ts.map +1 -0
  19. package/dist/githubActionOutput.js +33 -0
  20. package/dist/githubActionOutput.js.map +1 -0
  21. package/dist/githubActionReport.d.ts +2 -1
  22. package/dist/githubActionReport.d.ts.map +1 -1
  23. package/dist/githubActionReport.js +15 -182
  24. package/dist/githubActionReport.js.map +1 -1
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/reportTemplate.d.ts +3 -0
  30. package/dist/reportTemplate.d.ts.map +1 -1
  31. package/dist/reportTemplate.js +86 -29
  32. package/dist/reportTemplate.js.map +1 -1
  33. package/dist/screenshots.d.ts +3 -1
  34. package/dist/screenshots.d.ts.map +1 -1
  35. package/dist/screenshots.js +23 -0
  36. package/dist/screenshots.js.map +1 -1
  37. package/dist/types.d.ts +11 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/docs/evidence-policy.md +63 -0
  40. package/docs/spec-driven-flow.md +67 -8
  41. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
- import { spawn } from "node:child_process";
2
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
- import { dirname, join, relative, sep } from "node:path";
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
4
3
  import { tmpdir } from "node:os";
4
+ import { ftpConfig, listBuildNumbers, pathJoin, publicUrl, uploadDirectory, uploadFile, } from "./githubActionFtp.js";
5
+ import { writeGithubOutput, writeGithubSummary } from "./githubActionOutput.js";
5
6
  export async function publishGithubActionReport(options) {
6
7
  const reportDir = value(options, "report-dir", "FEATURE_SPEC_REPORT_DIR") ??
7
8
  "test-results/spec-report";
@@ -15,13 +16,13 @@ export async function publishGithubActionReport(options) {
15
16
  "## Feature Spec Report",
16
17
  "",
17
18
  `<p><strong>Hosted HTML report:</strong> <a href=\"${escapeHtml(reportUrl)}\">${escapeHtml(reportName)}</a></p>`,
18
- `<p><strong>Report index:</strong> <a href=\"${escapeHtml(indexUrl(config.baseUrl))}\">all builds</a></p>`,
19
+ `<p><strong>Build report index:</strong> <a href=\"${escapeHtml(buildIndexUrl(config.baseUrl))}\">all builds</a></p>`,
19
20
  "",
20
21
  ].join("\n"));
21
22
  await writeGithubOutput({
22
23
  "upload-github-artifact": "false",
23
24
  "report-url": reportUrl,
24
- "index-url": indexUrl(config.baseUrl),
25
+ "index-url": buildIndexUrl(config.baseUrl),
25
26
  });
26
27
  console.log(`Feature spec report uploaded to ${reportUrl}`);
27
28
  return;
@@ -39,165 +40,26 @@ export async function publishGithubActionReport(options) {
39
40
  });
40
41
  }
41
42
  async function publishReportToFtp(reportDir, config) {
42
- const buildRemoteDir = pathJoin(config.remoteDir, config.buildNumber);
43
+ const buildsRoot = pathJoin(config.remoteDir, "build");
44
+ const buildRemoteDir = pathJoin(buildsRoot, config.buildNumber);
43
45
  await uploadDirectory(reportDir, buildRemoteDir, config);
44
- const builds = await listBuildNumbers(config.remoteDir, config);
46
+ const builds = await listBuildNumbers(buildsRoot, config);
45
47
  if (!builds.includes(config.buildNumber))
46
48
  builds.push(config.buildNumber);
47
49
  builds.sort((a, b) => Number(b) - Number(a));
48
50
  const localIndex = join(tmpdir(), `feature-spec-md-build-index-${process.pid}.html`);
49
51
  await writeFile(localIndex, renderBuildIndex(config.baseUrl, builds), "utf8");
50
- await uploadFile(localIndex, pathJoin(config.remoteDir, "index.html"), config);
52
+ await uploadFile(localIndex, pathJoin(buildsRoot, "index.html"), config);
51
53
  return buildUrl(config.baseUrl, config.buildNumber);
52
54
  }
53
- async function uploadDirectory(localDir, remoteDir, config) {
54
- const files = await walkFiles(localDir);
55
- if (!files.length) {
56
- throw new Error(`Report directory contains no files: ${localDir}`);
57
- }
58
- for (const file of files) {
59
- const remotePath = pathJoin(remoteDir, relative(localDir, file).split(sep).join("/"));
60
- await uploadFile(file, remotePath, config);
61
- }
62
- }
63
- async function uploadFile(localFile, remotePath, config) {
64
- await runCurl([
65
- "--silent",
66
- "--show-error",
67
- "--fail",
68
- "--ftp-create-dirs",
69
- "-u",
70
- `${config.user}:${config.password}`,
71
- "-T",
72
- localFile,
73
- ftpUrl(config, remotePath),
74
- ]);
75
- }
76
- async function listBuildNumbers(remoteDir, config) {
77
- try {
78
- const listing = await listRemoteDirectory(remoteDir, config);
79
- return Array.from(new Set(parseBuildNumbersFromDirectoryListing(listing)));
80
- }
81
- catch {
82
- return [];
83
- }
84
- }
85
- async function listRemoteDirectory(remoteDir, config) {
86
- const directoryUrl = ftpUrl(config, asDirectoryPath(remoteDir));
87
- const commonArgs = [
88
- "--silent",
89
- "--show-error",
90
- "--fail",
91
- "-u",
92
- `${config.user}:${config.password}`,
93
- ];
94
- try {
95
- return await runCurl([...commonArgs, "--list-only", directoryUrl]);
96
- }
97
- catch {
98
- return runCurl([...commonArgs, directoryUrl]);
99
- }
100
- }
101
- function parseBuildNumbersFromDirectoryListing(listing) {
102
- return listing
103
- .split(/\r?\n/)
104
- .flatMap((line) => {
105
- const hrefBuilds = Array.from(line.matchAll(/href=["'][^"']*?(\d+)\/?["']/gi), (match) => match[1]);
106
- const lastToken = line.trim().split(/\s+/).at(-1) ?? "";
107
- return /^\d+$/.test(lastToken) ? [...hrefBuilds, lastToken] : hrefBuilds;
108
- })
109
- .filter((name) => /^\d+$/.test(name));
110
- }
111
- async function runCurl(args) {
112
- return new Promise((resolve, reject) => {
113
- const child = spawn("curl", args, { stdio: ["ignore", "pipe", "pipe"] });
114
- let stdout = "";
115
- let stderr = "";
116
- child.stdout.on("data", (chunk) => {
117
- stdout += chunk.toString();
118
- });
119
- child.stderr.on("data", (chunk) => {
120
- stderr += chunk.toString();
121
- });
122
- child.on("error", reject);
123
- child.on("close", (code) => {
124
- if (code === 0) {
125
- resolve(stdout);
126
- return;
127
- }
128
- reject(new Error(stderr.trim() || `curl exited with status ${code}`));
129
- });
130
- });
131
- }
132
- async function walkFiles(dir) {
133
- const entries = await readdir(dir, { withFileTypes: true });
134
- const files = await Promise.all(entries.map(async (entry) => {
135
- const fullPath = join(dir, entry.name);
136
- if (entry.isDirectory())
137
- return walkFiles(fullPath);
138
- if (entry.isFile())
139
- return [fullPath];
140
- return [];
141
- }));
142
- return files.flat();
143
- }
144
- function ftpConfig(options) {
145
- const host = required(options, "ftp-host", "FEATURE_SPEC_FTP_HOST");
146
- const user = required(options, "ftp-user", "FEATURE_SPEC_FTP_USER");
147
- const password = required(options, "ftp-password", "FEATURE_SPEC_FTP_PASSWORD");
148
- const baseUrl = required(options, "base-url", "FEATURE_SPEC_REPORT_BASE_URL");
149
- const remoteDir = value(options, "ftp-remote-dir", "FEATURE_SPEC_FTP_REMOTE_DIR") ?? "";
150
- const buildNumber = value(options, "build-number", "GITHUB_RUN_NUMBER") ??
151
- value(options, "build-number", "FEATURE_SPEC_BUILD_NUMBER") ??
152
- "local";
153
- const secure = booleanValue(value(options, "ftp-secure", "FEATURE_SPEC_FTP_SECURE"));
154
- const port = value(options, "ftp-port", "FEATURE_SPEC_FTP_PORT");
155
- if (!/^\d+$/.test(buildNumber)) {
156
- throw new Error(`Build number must be numeric for FTP publishing: ${buildNumber}`);
157
- }
158
- return { host, user, password, port, secure, remoteDir, baseUrl, buildNumber };
159
- }
160
55
  function publishMode(value) {
161
56
  if (value === "ftp" || value === "artifact")
162
57
  return value;
163
58
  throw new Error(`Unknown report publish mode: ${value}`);
164
59
  }
165
- function required(options, key, envKey) {
166
- const result = value(options, key, envKey);
167
- if (!result)
168
- throw new Error(`Missing required option --${key} or ${envKey}.`);
169
- return result;
170
- }
171
60
  function value(options, key, envKey) {
172
61
  return options[key] ?? process.env[envKey];
173
62
  }
174
- function booleanValue(value) {
175
- return value === "true" || value === "1" || value === "yes";
176
- }
177
- function ftpUrl(config, remotePath) {
178
- const protocol = config.secure ? "ftps" : "ftp";
179
- const port = config.port ? `:${config.port}` : "";
180
- const hasTrailingSlash = remotePath.endsWith("/");
181
- const encodedPath = remotePath
182
- .split("/")
183
- .filter(Boolean)
184
- .map(encodeURIComponent)
185
- .join("/");
186
- const directorySlash = hasTrailingSlash ? "/" : "";
187
- return `${protocol}://${config.host}${port}/${encodedPath}${directorySlash}`;
188
- }
189
- function asDirectoryPath(remotePath) {
190
- const path = pathJoin(remotePath);
191
- return path ? `${path}/` : "";
192
- }
193
- function pathJoin(...parts) {
194
- const joined = parts
195
- .flatMap((part) => part.split("/"))
196
- .map((part) => part.trim())
197
- .filter((part) => part && part !== ".")
198
- .join("/");
199
- return joined;
200
- }
201
63
  function renderBuildIndex(baseUrl, builds) {
202
64
  const generatedAt = new Date().toISOString();
203
65
  const rows = builds
@@ -208,14 +70,14 @@ function renderBuildIndex(baseUrl, builds) {
208
70
  <head>
209
71
  <meta charset=\"utf-8\">
210
72
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
211
- <title>Feature spec reports</title>
73
+ <title>Feature spec build reports</title>
212
74
  <style>
213
75
  body { font-family: system-ui, sans-serif; max-width: 48rem; margin: 3rem auto; padding: 0 1rem; line-height: 1.5; }
214
76
  li { margin: 0.35rem 0; }
215
77
  </style>
216
78
  </head>
217
79
  <body>
218
- <h1>Feature spec reports</h1>
80
+ <h1>Feature spec build reports</h1>
219
81
  <p>Generated ${escapeHtml(generatedAt)}.</p>
220
82
  <ol>
221
83
  ${rows}
@@ -225,13 +87,10 @@ ${rows}
225
87
  `;
226
88
  }
227
89
  function buildUrl(baseUrl, build) {
228
- return `${trimTrailingSlash(baseUrl)}/${encodeURIComponent(build)}/`;
229
- }
230
- function indexUrl(baseUrl) {
231
- return `${trimTrailingSlash(baseUrl)}/`;
90
+ return publicUrl(baseUrl, "build", build, "");
232
91
  }
233
- function trimTrailingSlash(value) {
234
- return value.replace(/\/+$/, "");
92
+ function buildIndexUrl(baseUrl) {
93
+ return publicUrl(baseUrl, "build", "");
235
94
  }
236
95
  function escapeHtml(value) {
237
96
  return value
@@ -240,30 +99,4 @@ function escapeHtml(value) {
240
99
  .replace(/>/g, "&gt;")
241
100
  .replace(/\"/g, "&quot;");
242
101
  }
243
- async function writeGithubSummary(markdown) {
244
- const summaryPath = process.env.GITHUB_STEP_SUMMARY;
245
- if (!summaryPath)
246
- return;
247
- await mkdir(dirname(summaryPath), { recursive: true });
248
- const existing = await readTextIfExists(summaryPath);
249
- await writeFile(summaryPath, `${existing}${markdown}`, "utf8");
250
- }
251
- async function writeGithubOutput(values) {
252
- const outputPath = process.env.GITHUB_OUTPUT;
253
- if (!outputPath)
254
- return;
255
- const lines = Object.entries(values)
256
- .map(([key, value]) => `${key}=${value}`)
257
- .join("\n");
258
- const existing = await readTextIfExists(outputPath);
259
- await writeFile(outputPath, `${existing}${lines}\n`, "utf8");
260
- }
261
- async function readTextIfExists(filePath) {
262
- try {
263
- return await readFile(filePath, "utf8");
264
- }
265
- catch {
266
- return "";
267
- }
268
- }
269
102
  //# sourceMappingURL=githubActionReport.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"githubActionReport.js","sourceRoot":"","sources":["../src/githubActionReport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"githubActionReport.js","sourceRoot":"","sources":["../src/githubActionReport.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export { expandFilePatterns } from "./filePatterns.js";
2
2
  export { buildCoverageSummary, checkFeatureSpecs, collectTestReferences, loadFeatureSpecs, parseFeatureSpec, parseTestReferences, validateCoverage, validateFeatureSpec, } from "./featureSpecs.js";
3
3
  export { renderHtmlReport } from "./reportTemplate.js";
4
- export { collectSpecScreenshots } from "./screenshots.js";
4
+ export { collectSpecScreenshots, validateScenarioScreenshots, } from "./screenshots.js";
5
5
  export { buildSpecCoverageSummary, checkSpecDocuments, collectSpecTestReferences, loadSpecDocuments, parseDesignSpec, parseModelSpec, parseSpecDocument, parseSpecTestReferences, parseStackSpec, validateDesignSpec, validateModelSpec, validateSpecDocument, validateSpecGraph, validateStackSpec, } from "./specDocuments.js";
6
- export type { CoverageItem, CoverageSummary, DesignFrontmatter, DesignSpec, FeatureFrontmatter, FeatureRule, FeatureScenario, FeatureSpec, FeatureStep, ModelItem, ModelReferenceFrontmatter, ModelSpec, RuleKeyword, SpecDocument, SpecFrontmatter, SpecScreenshot, StackSpec, StepKeyword, TestReference, ValidationIssue, } from "./types.js";
6
+ export type { CoverageItem, CoverageSummary, DesignFrontmatter, DesignSpec, FeatureFrontmatter, FeatureRule, FeatureScenario, FeatureSpec, FeatureStep, ModelItem, ModelReferenceFrontmatter, ModelSpec, RuleKeyword, ScenarioEvidencePolicy, ScenarioTestType, ScreenshotPolicy, SpecDocument, SpecFrontmatter, SpecScreenshot, StackSpec, StepKeyword, TestReference, ValidationIssue, } from "./types.js";
7
7
  /** Write text content to a path, creating the parent directory when needed. */
8
8
  export declare function writeTextFile(filePath: string, content: string): Promise<void>;
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,uBAAuB,EACvB,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,WAAW,EACX,WAAW,EACX,SAAS,EACT,yBAAyB,EACzB,SAAS,EACT,WAAW,EACX,YAAY,EACZ,eAAe,EACf,cAAc,EACd,SAAS,EACT,WAAW,EACX,aAAa,EACb,eAAe,GAChB,MAAM,YAAY,CAAC;AAEpB,+EAA+E;AAC/E,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAGpE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EACL,sBAAsB,EACtB,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,uBAAuB,EACvB,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,WAAW,EACX,WAAW,EACX,SAAS,EACT,yBAAyB,EACzB,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,SAAS,EACT,WAAW,EACX,aAAa,EACb,eAAe,GAChB,MAAM,YAAY,CAAC;AAEpB,+EAA+E;AAC/E,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAGpE"}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import path from "node:path";
7
7
  export { expandFilePatterns } from "./filePatterns.js";
8
8
  export { buildCoverageSummary, checkFeatureSpecs, collectTestReferences, loadFeatureSpecs, parseFeatureSpec, parseTestReferences, validateCoverage, validateFeatureSpec, } from "./featureSpecs.js";
9
9
  export { renderHtmlReport } from "./reportTemplate.js";
10
- export { collectSpecScreenshots } from "./screenshots.js";
10
+ export { collectSpecScreenshots, validateScenarioScreenshots, } from "./screenshots.js";
11
11
  export { buildSpecCoverageSummary, checkSpecDocuments, collectSpecTestReferences, loadSpecDocuments, parseDesignSpec, parseModelSpec, parseSpecDocument, parseSpecTestReferences, parseStackSpec, validateDesignSpec, validateModelSpec, validateSpecDocument, validateSpecGraph, validateStackSpec, } from "./specDocuments.js";
12
12
  /** Write text content to a path, creating the parent directory when needed. */
13
13
  export async function writeTextFile(filePath, content) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,uBAAuB,EACvB,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAwB5B,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAAgB,EAAE,OAAe;IACnE,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,MAAM,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAC7C,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EACL,sBAAsB,EACtB,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,uBAAuB,EACvB,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AA2B5B,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAAgB,EAAE,OAAe;IACnE,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,MAAM,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAC7C,CAAC"}
@@ -8,6 +8,9 @@ export type ReportOptions = {
8
8
  validationIssues?: ValidationIssue[];
9
9
  title?: string;
10
10
  generatedAt?: string;
11
+ githubBaseUrl?: string;
12
+ githubRef?: string;
13
+ repositoryUrl?: string;
11
14
  };
12
15
  /** Render a complete self-contained feature spec report as HTML. */
13
16
  export declare function renderHtmlReport(specs: FeatureSpec[], options?: ReportOptions): string;
@@ -1 +1 @@
1
- {"version":3,"file":"reportTemplate.d.ts","sourceRoot":"","sources":["../src/reportTemplate.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEV,eAAe,EACf,UAAU,EACV,WAAW,EAEX,SAAS,EACT,cAAc,EACd,SAAS,EACT,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAOF,oEAAoE;AACpE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,GAAE,aAAkB,UA+D5B"}
1
+ {"version":3,"file":"reportTemplate.d.ts","sourceRoot":"","sources":["../src/reportTemplate.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEV,eAAe,EACf,UAAU,EACV,WAAW,EAEX,SAAS,EACT,cAAc,EACd,SAAS,EAET,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAYF,oEAAoE;AACpE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,GAAE,aAAkB,UAuE5B"}
@@ -9,6 +9,10 @@ export function renderHtmlReport(specs, options = {}) {
9
9
  const title = options.title ?? "Feature Spec Report";
10
10
  const issues = options.validationIssues ?? [];
11
11
  const screenshots = options.screenshots ?? [];
12
+ const sourceLinks = {
13
+ githubBaseUrl: options.githubBaseUrl,
14
+ githubRef: options.githubRef,
15
+ };
12
16
  return `<!doctype html>
13
17
  <html lang="en">
14
18
  <head>
@@ -32,21 +36,25 @@ export function renderHtmlReport(specs, options = {}) {
32
36
  table{border-collapse:collapse;width:100%;font-size:14px}
33
37
  th,td{border:1px solid #d0d7de;padding:6px 8px;text-align:left;vertical-align:top}
34
38
  th{background:#f6f8fa}
39
+ h1 a{color:#0969da;text-decoration:underline;text-underline-offset:3px}
40
+ h1 a:hover{text-decoration-thickness:2px}
35
41
  .step{border-left:3px solid #d0d7de;margin:12px 0;padding:2px 0 2px 12px}
36
42
  .step p{margin:8px 0}
37
43
  .screenshots{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;margin:10px 0 14px}
38
44
  .screenshot{border:1px solid #d0d7de;border-radius:8px;overflow:hidden;background:#f6f8fa}
39
45
  .screenshot img{display:block;width:100%;height:auto}
40
46
  .screenshot figcaption{font-size:12px;padding:8px;color:#57606a}
41
- .coverage-detail{color:#57606a;font-size:12px}
47
+ .coverage-refs{display:inline-flex;gap:2px;margin-left:4px}
48
+ .coverage-ref{color:inherit;text-decoration:underline;text-underline-offset:2px}
49
+ .line-link{color:inherit;text-decoration:underline;text-underline-offset:2px}
42
50
  </style>
43
51
  </head>
44
52
  <body>
45
- <h1>${html(title)}</h1>
53
+ <h1>${renderReportTitle(title, options.repositoryUrl)}</h1>
46
54
  <p>Generated ${html(formatGeneratedAt(options.generatedAt))}.</p>
47
55
  ${renderIssues(issues)}
48
- ${renderModels(options.models ?? [], options.coverage)}
49
- ${specs.map((spec) => renderSpec(spec, options.coverage, screenshots)).join("\n")}
56
+ ${renderModels(options.models ?? [], options.coverage, sourceLinks)}
57
+ ${specs.map((spec) => renderSpec(spec, options.coverage, screenshots, sourceLinks)).join("\n")}
50
58
  <script>
51
59
  document.addEventListener("toggle", (event) => {
52
60
  const target = event.target;
@@ -67,6 +75,16 @@ export function renderHtmlReport(specs, options = {}) {
67
75
  </body>
68
76
  </html>`;
69
77
  }
78
+ function renderReportTitle(title, repositoryUrl) {
79
+ if (!repositoryUrl)
80
+ return html(title);
81
+ const prefix = "Feature Spec Report for ";
82
+ const attributes = `href="${html(repositoryUrl)}" target="_blank" rel="noopener noreferrer"`;
83
+ if (title.startsWith(prefix) && title.length > prefix.length) {
84
+ return `${html(prefix)}<a ${attributes}>${html(title.slice(prefix.length))}</a>`;
85
+ }
86
+ return `<a ${attributes}>${html(title)}</a>`;
87
+ }
70
88
  function formatGeneratedAt(value) {
71
89
  const date = value ? new Date(value) : new Date();
72
90
  if (Number.isNaN(date.getTime()))
@@ -108,7 +126,7 @@ function renderIssues(issues) {
108
126
  .map((issue) => `<li class="${issue.severity}"><code>${html(`${issue.filePath ?? ""}${issue.line ? `:${issue.line}` : ""}`)}</code> ${html(issue.message)}</li>`)
109
127
  .join("")}</ul></section>`;
110
128
  }
111
- function renderModels(models, coverage) {
129
+ function renderModels(models, coverage, sourceLinks = {}) {
112
130
  if (!models.length)
113
131
  return "";
114
132
  const modelCoverage = coverage?.modelCoverage ?? [];
@@ -129,7 +147,7 @@ function renderModels(models, coverage) {
129
147
  .map((item) => {
130
148
  const coverageItem = modelCoverage.find((candidate) => candidate.id === item.id);
131
149
  return `<details class="model-item">
132
- <summary><code>${html(item.id)}</code>: ${html(item.title)} ${coverageBadge(coverageItem?.covered)}${renderCoverageReferences(coverageItem)}</summary>
150
+ <summary><code>${html(item.id)}</code>: ${html(item.title)} ${coverageBadge(coverageItem?.covered, [], coverageItem, sourceLinks)}</summary>
133
151
  <div class="model-item-body">${renderModelItemBody(item.body)}</div>
134
152
  </details>`;
135
153
  })
@@ -138,7 +156,7 @@ function renderModels(models, coverage) {
138
156
  ? `<h4>Rules</h4><ul>${model.rules
139
157
  .map((rule) => {
140
158
  const item = ruleCoverage.find((coverageItem) => coverageItem.id === rule.id);
141
- return `<li><code>${html(rule.id)}</code>: ${html(rule.text)} ${ruleCoverageBadge(item, ruleScenarioIds(rule.id, ruleScenarioLinks))}${renderCoverageReferences(item)}</li>`;
159
+ return `<li><code>${html(rule.id)}</code>: ${html(rule.text)} ${ruleCoverageBadge(item, ruleScenarioIds(rule.id, ruleScenarioLinks), sourceLinks)}</li>`;
142
160
  })
143
161
  .join("")}</ul>`
144
162
  : ""}
@@ -146,7 +164,7 @@ function renderModels(models, coverage) {
146
164
  .join("\n")}
147
165
  </section>`;
148
166
  }
149
- function renderSpec(spec, coverage, screenshots = []) {
167
+ function renderSpec(spec, coverage, screenshots = [], sourceLinks = {}) {
150
168
  const screenshotsByLine = groupScreenshotsByLine(screenshots);
151
169
  const ruleCoverage = coverage?.ruleCoverage ?? [];
152
170
  const scenarioCoverage = coverage?.scenarioCoverage ?? [];
@@ -161,7 +179,7 @@ function renderSpec(spec, coverage, screenshots = []) {
161
179
  <ul>${spec.rules
162
180
  .map((rule) => {
163
181
  const item = ruleCoverage.find((coverageItem) => coverageItem.id === rule.id);
164
- return `<li><code>${html(rule.id)}</code>: ${html(rule.text)} ${ruleCoverageBadge(item, ruleScenarioIds(rule.id, ruleScenarioLinks))}${renderCoverageReferences(item)}</li>`;
182
+ return `<li><code>${html(rule.id)}</code>: ${html(rule.text)} ${ruleCoverageBadge(item, ruleScenarioIds(rule.id, ruleScenarioLinks), sourceLinks)}</li>`;
165
183
  })
166
184
  .join("")}</ul>
167
185
  <h3>Scenarios</h3>
@@ -171,9 +189,10 @@ function renderSpec(spec, coverage, screenshots = []) {
171
189
  (screenshotsByLine.get(screenshotKey(spec.filePath, step.line))
172
190
  ?.length ?? 0), 0);
173
191
  const scenarioRuleIds = ruleIdsForScenario(scenario.id, spec.rules.map((rule) => rule.id), ruleScenarioLinks);
192
+ const scenarioCoverageItem = scenarioCoverage.find((item) => item.id === scenario.id);
174
193
  return `<details class="scenario" data-has-images="${screenshotCount > 0 ? "true" : "false"}">
175
- <summary><code>${html(scenario.id)}</code>: ${html(scenario.title)} ${coverageBadge(scenarioCoverage.find((item) => item.id === scenario.id)?.covered)} <span class="badge">${screenshotCount} screenshot${screenshotCount === 1 ? "" : "s"}</span></summary>
176
- <div class="scenario-body">${renderScenarioRuleCoverage(scenarioRuleIds)}${scenario.steps.map((step) => renderStep(spec, step, screenshotsByLine)).join("")}</div>
194
+ <summary><code>${html(scenario.id)}</code>: ${html(scenario.title)} ${coverageBadge(scenarioCoverageItem?.covered, [], scenarioCoverageItem, sourceLinks)} <span class="badge">${screenshotCount} screenshot${screenshotCount === 1 ? "" : "s"}</span></summary>
195
+ <div class="scenario-body">${renderScenarioRuleCoverage(scenarioRuleIds)}${scenario.steps.map((step) => renderStep(spec, step, screenshotsByLine, sourceLinks)).join("")}</div>
177
196
  </details>`;
178
197
  })
179
198
  .join("\n")}
@@ -259,12 +278,19 @@ function renderTable(lines) {
259
278
  .join("")}</tr>`)
260
279
  .join("")}</tbody></table></div>`;
261
280
  }
262
- function renderStep(spec, step, screenshotsByLine) {
281
+ function renderStep(spec, step, screenshotsByLine, sourceLinks) {
263
282
  const screenshots = screenshotsByLine.get(screenshotKey(spec.filePath, step.line)) ?? [];
264
283
  const evidenceBadge = screenshots.length
265
284
  ? `<span class="badge ok">${screenshots.length} screenshot${screenshots.length === 1 ? "" : "s"}</span>`
266
285
  : `<span class="badge missing">missing screenshot</span>`;
267
- return `<div class="step"><p><strong>${html(step.keyword)}</strong> ${html(step.text)} <span class="badge">line ${step.line}</span> ${evidenceBadge}</p>${renderScreenshots(screenshots)}</div>`;
286
+ return `<div class="step"><p><strong>${html(step.keyword)}</strong> ${html(step.text)} ${renderLineBadge(spec.filePath, step.line, sourceLinks)} ${evidenceBadge}</p>${renderScreenshots(screenshots)}</div>`;
287
+ }
288
+ function renderLineBadge(filePath, line, sourceLinks) {
289
+ const label = `line ${line}`;
290
+ const url = sourceLineUrl(filePath, line, sourceLinks);
291
+ if (!url)
292
+ return `<span class="badge">${html(label)}</span>`;
293
+ return `<a class="badge line-link" href="${html(url)}" title="${html(`${filePath}:${line}`)}" target="_blank" rel="noopener noreferrer">${html(label)}</a>`;
268
294
  }
269
295
  function renderScreenshots(screenshots) {
270
296
  if (!screenshots.length)
@@ -273,34 +299,65 @@ function renderScreenshots(screenshots) {
273
299
  .map((screenshot) => `<figure class="screenshot"><img src="${html(screenshot.path)}" alt="${html(screenshot.title ?? `Screenshot for ${screenshot.specPath}:${screenshot.line}`)}"><figcaption>${html(screenshot.title ?? `${screenshot.specPath}:${screenshot.line}`)}</figcaption></figure>`)
274
300
  .join("")}</div>`;
275
301
  }
276
- function coverageBadge(covered, suffixes = []) {
302
+ function coverageBadge(covered, suffixes = [], item, sourceLinks = {}) {
277
303
  return covered === undefined
278
304
  ? ""
279
305
  : covered
280
- ? `<span class="badge ok">covered${suffixes.length ? ` by ${suffixes.map(html).join(" ")}` : ""}</span>`
306
+ ? `<span class="badge ok">covered${suffixes.length ? ` by ${suffixes.map(html).join(" ")}` : ""}${renderCoverageReferenceMarkers(item, sourceLinks)}</span>`
281
307
  : `<span class="badge missing">missing coverage</span>`;
282
308
  }
283
- function ruleCoverageBadge(ruleCoverage, scenarioIds) {
309
+ function ruleCoverageBadge(ruleCoverage, scenarioIds, sourceLinks) {
284
310
  if (ruleCoverage?.covered && !scenarioIds.length) {
285
- return coverageBadge(true, ["direct test"]);
311
+ return coverageBadge(true, ["direct test"], ruleCoverage, sourceLinks);
286
312
  }
287
- return coverageBadge(ruleCoverage?.covered, scenarioIds);
313
+ return coverageBadge(ruleCoverage?.covered, scenarioIds, ruleCoverage, sourceLinks);
288
314
  }
289
- function renderCoverageReferences(item) {
290
- const labels = coverageReferenceLabels(item);
291
- if (!labels.length)
315
+ function renderCoverageReferenceMarkers(item, sourceLinks) {
316
+ const references = uniqueCoverageReferences(item);
317
+ if (!references.length)
292
318
  return "";
293
- return ` <span class="coverage-detail">via ${labels
294
- .map((label) => `<code>${html(label)}</code>`)
295
- .join(" ")}</span>`;
319
+ return ` <span class="coverage-refs">${references
320
+ .map((reference, index) => renderCoverageReferenceMarker(reference, index + 1, sourceLinks))
321
+ .join(",")}</span>`;
322
+ }
323
+ function renderCoverageReferenceMarker(reference, index, sourceLinks) {
324
+ const label = coverageReferenceLabel(reference);
325
+ const url = coverageReferenceUrl(reference, sourceLinks);
326
+ if (!url)
327
+ return `<span class="coverage-ref" title="${html(label)}">${index}</span>`;
328
+ return `<a class="coverage-ref" href="${html(url)}" title="${html(label)}" target="_blank" rel="noopener noreferrer">${index}</a>`;
296
329
  }
297
- function coverageReferenceLabels(item) {
330
+ function uniqueCoverageReferences(item) {
298
331
  if (!item)
299
332
  return [];
300
- return Array.from(new Set(item.references.map((reference) => {
301
- const line = reference.line ? `:${reference.line}` : "";
302
- return `${reference.filePath}${line}`;
303
- })));
333
+ const seen = new Set();
334
+ const references = [];
335
+ for (const reference of item.references) {
336
+ const key = coverageReferenceLabel(reference);
337
+ if (seen.has(key))
338
+ continue;
339
+ seen.add(key);
340
+ references.push(reference);
341
+ }
342
+ return references;
343
+ }
344
+ function coverageReferenceLabel(reference) {
345
+ const line = reference.line ? `:${reference.line}` : "";
346
+ return `${reference.filePath}${line}`;
347
+ }
348
+ function coverageReferenceUrl(reference, sourceLinks) {
349
+ return sourceLineUrl(reference.filePath, reference.line, sourceLinks);
350
+ }
351
+ function sourceLineUrl(filePath, line, sourceLinks) {
352
+ if (!sourceLinks.githubBaseUrl || !sourceLinks.githubRef)
353
+ return undefined;
354
+ const baseUrl = sourceLinks.githubBaseUrl.replace(/\/$/, "");
355
+ const ref = encodeURIComponent(sourceLinks.githubRef);
356
+ const encodedFilePath = filePath
357
+ .split("/")
358
+ .map((segment) => encodeURIComponent(segment))
359
+ .join("/");
360
+ return `${baseUrl}/blob/${ref}/${encodedFilePath}#L${line}`;
304
361
  }
305
362
  function ruleScenarioIds(ruleId, ruleScenarioLinks) {
306
363
  return Array.from(new Set(ruleScenarioLinks