@trailmark/core 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.
Files changed (94) hide show
  1. package/dist/baseline.d.ts +21 -0
  2. package/dist/baseline.d.ts.map +1 -0
  3. package/dist/baseline.js +187 -0
  4. package/dist/baseline.js.map +1 -0
  5. package/dist/diff.d.ts +3 -0
  6. package/dist/diff.d.ts.map +1 -0
  7. package/dist/diff.js +120 -0
  8. package/dist/diff.js.map +1 -0
  9. package/dist/fingerprint.d.ts +8 -0
  10. package/dist/fingerprint.d.ts.map +1 -0
  11. package/dist/fingerprint.js +85 -0
  12. package/dist/fingerprint.js.map +1 -0
  13. package/dist/hash.d.ts +4 -0
  14. package/dist/hash.d.ts.map +1 -0
  15. package/dist/hash.js +29 -0
  16. package/dist/hash.js.map +1 -0
  17. package/dist/impact.d.ts +5 -0
  18. package/dist/impact.d.ts.map +1 -0
  19. package/dist/impact.js +27 -0
  20. package/dist/impact.js.map +1 -0
  21. package/dist/index.d.ts +12 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +28 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/issue.d.ts +10 -0
  26. package/dist/issue.d.ts.map +1 -0
  27. package/dist/issue.js +40 -0
  28. package/dist/issue.js.map +1 -0
  29. package/dist/normalize.d.ts +3 -0
  30. package/dist/normalize.d.ts.map +1 -0
  31. package/dist/normalize.js +59 -0
  32. package/dist/normalize.js.map +1 -0
  33. package/dist/report/buildReport.d.ts +3 -0
  34. package/dist/report/buildReport.d.ts.map +1 -0
  35. package/dist/report/buildReport.js +223 -0
  36. package/dist/report/buildReport.js.map +1 -0
  37. package/dist/report/formatConsole.d.ts +3 -0
  38. package/dist/report/formatConsole.d.ts.map +1 -0
  39. package/dist/report/formatConsole.js +112 -0
  40. package/dist/report/formatConsole.js.map +1 -0
  41. package/dist/report/index.d.ts +4 -0
  42. package/dist/report/index.d.ts.map +1 -0
  43. package/dist/report/index.js +20 -0
  44. package/dist/report/index.js.map +1 -0
  45. package/dist/report/sort.d.ts +9 -0
  46. package/dist/report/sort.d.ts.map +1 -0
  47. package/dist/report/sort.js +20 -0
  48. package/dist/report/sort.js.map +1 -0
  49. package/dist/report/types.d.ts +85 -0
  50. package/dist/report/types.d.ts.map +1 -0
  51. package/dist/report/types.js +3 -0
  52. package/dist/report/types.js.map +1 -0
  53. package/dist/run.d.ts +8 -0
  54. package/dist/run.d.ts.map +1 -0
  55. package/dist/run.js +20 -0
  56. package/dist/run.js.map +1 -0
  57. package/dist/scope.d.ts +5 -0
  58. package/dist/scope.d.ts.map +1 -0
  59. package/dist/scope.js +23 -0
  60. package/dist/scope.js.map +1 -0
  61. package/dist/types.d.ts +150 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +3 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/url.d.ts +3 -0
  66. package/dist/url.d.ts.map +1 -0
  67. package/dist/url.js +82 -0
  68. package/dist/url.js.map +1 -0
  69. package/package.json +23 -0
  70. package/src/baseline.ts +217 -0
  71. package/src/diff.ts +150 -0
  72. package/src/fingerprint.ts +107 -0
  73. package/src/hash.ts +29 -0
  74. package/src/impact.ts +27 -0
  75. package/src/index.ts +11 -0
  76. package/src/issue.ts +49 -0
  77. package/src/normalize.ts +66 -0
  78. package/src/report/buildReport.ts +276 -0
  79. package/src/report/formatConsole.ts +137 -0
  80. package/src/report/index.ts +3 -0
  81. package/src/report/sort.ts +28 -0
  82. package/src/report/types.ts +101 -0
  83. package/src/run.ts +22 -0
  84. package/src/scope.ts +25 -0
  85. package/src/types.ts +167 -0
  86. package/src/url.ts +102 -0
  87. package/test/baseline.test.ts +151 -0
  88. package/test/diff.test.ts +113 -0
  89. package/test/fingerprint.test.ts +41 -0
  90. package/test/issue.test.ts +79 -0
  91. package/test/report.build.test.ts +191 -0
  92. package/test/report.console.test.ts +102 -0
  93. package/test/url.test.ts +25 -0
  94. package/tsconfig.json +8 -0
@@ -0,0 +1,276 @@
1
+ import { hashObject } from "../hash"
2
+ import {
3
+ BuildReportInput,
4
+ TrailmarkReport,
5
+ TrailmarkReportIssue,
6
+ TrailmarkReportOccurrence,
7
+ } from "./types"
8
+ import { TrailmarkAttachment, TrailmarkFinding } from "../types"
9
+ import { impactKey, impactRank } from "../impact"
10
+ import { compareIssueLike } from "./sort"
11
+
12
+ type MutableIssue = TrailmarkReportIssue & {
13
+ occurrenceKeys: Set<string>
14
+ }
15
+
16
+ function dedupeAndSortTags(tags: string[]): string[] {
17
+ return Array.from(new Set(tags)).sort((left, right) => left.localeCompare(right))
18
+ }
19
+
20
+ function compareOccurrence(
21
+ left: TrailmarkReportOccurrence,
22
+ right: TrailmarkReportOccurrence,
23
+ ): number {
24
+ const projectDiff = (left.projectName ?? "").localeCompare(right.projectName ?? "")
25
+ if (projectDiff !== 0) {
26
+ return projectDiff
27
+ }
28
+
29
+ const testDiff = (left.testId ?? "").localeCompare(right.testId ?? "")
30
+ if (testDiff !== 0) {
31
+ return testDiff
32
+ }
33
+
34
+ const urlDiff = left.url.localeCompare(right.url)
35
+ if (urlDiff !== 0) {
36
+ return urlDiff
37
+ }
38
+
39
+ const timestampDiff = left.timestamp.localeCompare(right.timestamp)
40
+ if (timestampDiff !== 0) {
41
+ return timestampDiff
42
+ }
43
+
44
+ return (left.occurrenceId ?? "").localeCompare(right.occurrenceId ?? "")
45
+ }
46
+
47
+ function toSortedRecord(map: Map<string, number>): Record<string, number> {
48
+ const sorted = Array.from(map.entries()).sort((left, right) => left[0].localeCompare(right[0]))
49
+ const record: Record<string, number> = {}
50
+ for (const [key, value] of sorted) {
51
+ record[key] = value
52
+ }
53
+
54
+ return record
55
+ }
56
+
57
+ function toSortedAttachment(attachment: TrailmarkAttachment): TrailmarkAttachment {
58
+ return {
59
+ kind: attachment.kind,
60
+ path: attachment.path,
61
+ error: attachment.error,
62
+ }
63
+ }
64
+
65
+ function compareAttachment(left: TrailmarkAttachment, right: TrailmarkAttachment): number {
66
+ const kindDiff = left.kind.localeCompare(right.kind)
67
+ if (kindDiff !== 0) {
68
+ return kindDiff
69
+ }
70
+
71
+ const pathDiff = (left.path ?? "").localeCompare(right.path ?? "")
72
+ if (pathDiff !== 0) {
73
+ return pathDiff
74
+ }
75
+
76
+ return (left.error ?? "").localeCompare(right.error ?? "")
77
+ }
78
+
79
+ function resolveAttachments(
80
+ finding: TrailmarkFinding,
81
+ artifacts: BuildReportInput["artifacts"],
82
+ ): TrailmarkAttachment[] {
83
+ const fromOccurrenceId = finding.occurrenceId
84
+ ? artifacts?.byOccurrenceId?.[finding.occurrenceId]
85
+ : undefined
86
+ const fromIssueId = artifacts?.byIssueId?.[finding.issueId]
87
+ const source = fromOccurrenceId ?? finding.attachments ?? fromIssueId ?? []
88
+
89
+ return [...source].map(toSortedAttachment).sort(compareAttachment)
90
+ }
91
+
92
+ function occurrenceKey(finding: TrailmarkFinding, occurrence: TrailmarkReportOccurrence): string {
93
+ if (finding.occurrenceId) {
94
+ return finding.occurrenceId
95
+ }
96
+
97
+ return hashObject({
98
+ issueId: finding.issueId,
99
+ testId: occurrence.testId,
100
+ projectName: occurrence.projectName,
101
+ url: occurrence.url,
102
+ selector: finding.target[0] ?? finding.selectorHint ?? "",
103
+ })
104
+ }
105
+
106
+ function findingSortKey(finding: TrailmarkFinding): string {
107
+ return [
108
+ impactRank(finding.impact),
109
+ finding.ruleId,
110
+ finding.pageKey,
111
+ finding.issueId,
112
+ finding.meta?.projectName ?? "",
113
+ finding.meta?.testId ?? "",
114
+ finding.target[0] ?? finding.selectorHint ?? "",
115
+ ].join("|")
116
+ }
117
+
118
+ function uniqueSorted(values: string[]): string[] {
119
+ return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right))
120
+ }
121
+
122
+ export function buildReport(input: BuildReportInput): TrailmarkReport {
123
+ const createdAt = input.createdAt ?? new Date().toISOString()
124
+ const issuesById = new Map<string, MutableIssue>()
125
+ const scannedPages = new Set<string>()
126
+ const sortedFindings = [...input.run.findings].sort((left, right) =>
127
+ findingSortKey(left).localeCompare(findingSortKey(right)),
128
+ )
129
+
130
+ for (const finding of sortedFindings) {
131
+ scannedPages.add(finding.pageKey)
132
+
133
+ const existing = issuesById.get(finding.issueId)
134
+ const issue = existing ?? {
135
+ issueId: finding.issueId,
136
+ ruleId: finding.ruleId,
137
+ impact: finding.impact,
138
+ tags: dedupeAndSortTags(finding.tags),
139
+ pageKey: finding.pageKey,
140
+ url: finding.pageUrl,
141
+ target: [...finding.target],
142
+ selectorHint: finding.selectorHint,
143
+ failureSummary: finding.failureSummary,
144
+ occurrences: [],
145
+ occurrenceKeys: new Set<string>(),
146
+ }
147
+
148
+ if (!existing) {
149
+ issuesById.set(finding.issueId, issue)
150
+ }
151
+
152
+ const occurrence: TrailmarkReportOccurrence = {
153
+ occurrenceId: finding.occurrenceId,
154
+ testId: finding.meta?.testId,
155
+ projectName: finding.meta?.projectName,
156
+ url: finding.pageUrl,
157
+ timestamp: finding.occurredAt ?? input.run.finishedAt ?? input.run.startedAt,
158
+ attachments: resolveAttachments(finding, input.artifacts),
159
+ }
160
+
161
+ const key = occurrenceKey(finding, occurrence)
162
+ if (issue.occurrenceKeys.has(key)) {
163
+ continue
164
+ }
165
+
166
+ issue.occurrenceKeys.add(key)
167
+ issue.occurrences.push(occurrence)
168
+ }
169
+
170
+ const issues = Array.from(issuesById.values())
171
+ .map((issue) => ({
172
+ issueId: issue.issueId,
173
+ ruleId: issue.ruleId,
174
+ impact: issue.impact,
175
+ tags: issue.tags,
176
+ pageKey: issue.pageKey,
177
+ url: issue.url,
178
+ target: issue.target,
179
+ selectorHint: issue.selectorHint,
180
+ failureSummary: issue.failureSummary,
181
+ occurrences: [...issue.occurrences].sort(compareOccurrence),
182
+ }))
183
+ .sort(compareIssueLike)
184
+
185
+ const byImpact = new Map<string, number>()
186
+ const byRuleId = new Map<string, number>()
187
+ const byPageKey = new Map<string, number>()
188
+
189
+ for (const issue of issues) {
190
+ const impact = impactKey(issue.impact)
191
+ byImpact.set(impact, (byImpact.get(impact) ?? 0) + 1)
192
+ byRuleId.set(issue.ruleId, (byRuleId.get(issue.ruleId) ?? 0) + 1)
193
+ byPageKey.set(issue.pageKey, (byPageKey.get(issue.pageKey) ?? 0) + 1)
194
+ }
195
+
196
+ const newIssueIds = uniqueSorted(input.diff?.newFindings.map((finding) => finding.issueId) ?? [])
197
+ const existingIssueIds = uniqueSorted(
198
+ input.diff?.existing.map((entry) => entry.finding.issueId) ?? [],
199
+ )
200
+ const resolvedIssueIds = uniqueSorted(input.diff?.resolved.map((entry) => entry.issueId) ?? [])
201
+
202
+ const summary = {
203
+ scannedPages: scannedPages.size,
204
+ totalIssues: issues.length,
205
+ totalOccurrences: sortedFindings.length,
206
+ scanErrors: input.run.scanErrors.length,
207
+ newIssues: newIssueIds.length,
208
+ existingIssues: existingIssueIds.length,
209
+ resolvedIssues: resolvedIssueIds.length,
210
+ byImpact: toSortedRecord(byImpact),
211
+ byRuleId: toSortedRecord(byRuleId),
212
+ byPageKey: toSortedRecord(byPageKey),
213
+ }
214
+
215
+ const report: TrailmarkReport = {
216
+ run: {
217
+ reportVersion: 2,
218
+ mode: input.mode,
219
+ createdAt,
220
+ startedAt: input.run.startedAt,
221
+ finishedAt: input.run.finishedAt,
222
+ configHash: input.configHash,
223
+ tool: input.run.tool,
224
+ engine: {
225
+ scanner: input.run.tool.scanner,
226
+ runtime: "playwright",
227
+ },
228
+ metadata: input.run.metadata,
229
+ },
230
+ summary,
231
+ issues,
232
+ }
233
+
234
+ if (input.diff) {
235
+ const issuesByIssueId = new Map(issues.map((issue) => [issue.issueId, issue]))
236
+ const newIssues = newIssueIds
237
+ .map((issueId) => {
238
+ const issue = issuesByIssueId.get(issueId)
239
+ if (!issue) {
240
+ const fallback = input.diff?.newFindings.find((finding) => finding.issueId === issueId)
241
+ if (!fallback) {
242
+ return undefined
243
+ }
244
+
245
+ return {
246
+ issueId,
247
+ ruleId: fallback.ruleId,
248
+ impact: fallback.impact,
249
+ pageKey: fallback.pageKey,
250
+ selectorHint: fallback.target[0] ?? fallback.selectorHint,
251
+ occurrenceCount: 1,
252
+ }
253
+ }
254
+
255
+ return {
256
+ issueId,
257
+ ruleId: issue.ruleId,
258
+ impact: issue.impact,
259
+ pageKey: issue.pageKey,
260
+ selectorHint: issue.target[0] ?? issue.selectorHint,
261
+ occurrenceCount: issue.occurrences.length,
262
+ }
263
+ })
264
+ .filter((entry): entry is NonNullable<typeof entry> => Boolean(entry))
265
+ .sort(compareIssueLike)
266
+
267
+ report.diff = {
268
+ newIssueIds,
269
+ existingIssueIds,
270
+ resolvedIssueIds,
271
+ newIssues,
272
+ }
273
+ }
274
+
275
+ return report
276
+ }
@@ -0,0 +1,137 @@
1
+ import { FormatConsoleOptions, TrailmarkReport, TrailmarkReportIssue } from "./types"
2
+ import { TrailmarkImpactKey } from "../types"
3
+ import { impactKey, impactRank } from "../impact"
4
+ import { compareIssueLike } from "./sort"
5
+
6
+ function topPages(issues: TrailmarkReportIssue[], limit: number): string {
7
+ const counts = new Map<string, number>()
8
+ for (const issue of issues) {
9
+ counts.set(issue.pageKey, (counts.get(issue.pageKey) ?? 0) + 1)
10
+ }
11
+
12
+ return Array.from(counts.entries())
13
+ .sort((left, right) => {
14
+ const countDiff = right[1] - left[1]
15
+ if (countDiff !== 0) {
16
+ return countDiff
17
+ }
18
+ return left[0].localeCompare(right[0])
19
+ })
20
+ .slice(0, limit)
21
+ .map(([page]) => page)
22
+ .join(", ")
23
+ }
24
+
25
+ function firstArtifactPath(issue: TrailmarkReportIssue): string | undefined {
26
+ for (const occurrence of issue.occurrences) {
27
+ for (const attachment of occurrence.attachments) {
28
+ if (attachment.path) {
29
+ return attachment.path
30
+ }
31
+ }
32
+ }
33
+
34
+ return undefined
35
+ }
36
+
37
+ function firstReproHint(issue: TrailmarkReportIssue): string {
38
+ const occurrence = issue.occurrences[0]
39
+ if (!occurrence) {
40
+ return `url=${issue.url}`
41
+ }
42
+
43
+ const segments: string[] = []
44
+ if (occurrence.testId) {
45
+ segments.push(`testId=${occurrence.testId}`)
46
+ }
47
+ segments.push(`url=${occurrence.url}`)
48
+ return segments.join(" ")
49
+ }
50
+
51
+ export function formatConsoleReport(
52
+ report: TrailmarkReport,
53
+ options: FormatConsoleOptions = {},
54
+ ): string {
55
+ const failOnImpact = options.failOnImpact ?? ["serious", "critical"]
56
+ const topPagesPerRule = options.topPagesPerRule ?? 3
57
+ const lines: string[] = []
58
+
59
+ lines.push(
60
+ `trailmark: scanned pages=${report.summary.scannedPages} | total issues=${report.summary.totalIssues} | NEW=${report.summary.newIssues} | fail on=${failOnImpact.join(", ")}`,
61
+ )
62
+
63
+ if (!report.diff) {
64
+ if (report.summary.scanErrors > 0) {
65
+ lines.push(`trailmark: scan errors=${report.summary.scanErrors}`)
66
+ }
67
+ return lines.join("\n")
68
+ }
69
+
70
+ if (report.diff.newIssueIds.length === 0) {
71
+ lines.push("trailmark: no new accessibility violations compared to baseline.")
72
+ if (report.summary.scanErrors > 0) {
73
+ lines.push(`trailmark: scan errors=${report.summary.scanErrors}`)
74
+ }
75
+ return lines.join("\n")
76
+ }
77
+
78
+ const newIssueIds = new Set(report.diff.newIssueIds)
79
+ const newIssues = report.issues
80
+ .filter((issue) => newIssueIds.has(issue.issueId))
81
+ .sort(compareIssueLike)
82
+
83
+ const byImpact = new Map<TrailmarkImpactKey, TrailmarkReportIssue[]>()
84
+ for (const issue of newIssues) {
85
+ const impact = impactKey(issue.impact)
86
+ const list = byImpact.get(impact) ?? []
87
+ list.push(issue)
88
+ byImpact.set(impact, list)
89
+ }
90
+
91
+ const sortedImpactKeys = Array.from(byImpact.keys()).sort((left, right) => {
92
+ const leftRank = impactRank(left)
93
+ const rightRank = impactRank(right)
94
+ if (leftRank !== rightRank) {
95
+ return leftRank - rightRank
96
+ }
97
+
98
+ return left.localeCompare(right)
99
+ })
100
+
101
+ for (const impact of sortedImpactKeys) {
102
+ const issues = byImpact.get(impact) ?? []
103
+ lines.push(`trailmark: [${impact}] ${issues.length} NEW issue(s)`)
104
+
105
+ const byRule = new Map<string, TrailmarkReportIssue[]>()
106
+ for (const issue of issues) {
107
+ const list = byRule.get(issue.ruleId) ?? []
108
+ list.push(issue)
109
+ byRule.set(issue.ruleId, list)
110
+ }
111
+
112
+ const sortedRuleIds = Array.from(byRule.keys()).sort((left, right) => left.localeCompare(right))
113
+ for (const ruleId of sortedRuleIds) {
114
+ const ruleIssues = (byRule.get(ruleId) ?? []).sort(compareIssueLike)
115
+ const pages = topPages(ruleIssues, topPagesPerRule)
116
+ lines.push(` rule ${ruleId} (${ruleIssues.length}) top pages: ${pages || "n/a"}`)
117
+
118
+ for (const issue of ruleIssues) {
119
+ const locator = issue.target[0] ?? issue.selectorHint ?? "<unknown-target>"
120
+ lines.push(` - ${impact} | ${issue.ruleId} | ${issue.pageKey} | ${locator}`)
121
+
122
+ const artifactPath = firstArtifactPath(issue)
123
+ if (artifactPath) {
124
+ lines.push(` artifact: ${artifactPath}`)
125
+ }
126
+
127
+ lines.push(` reproduce: ${firstReproHint(issue)}`)
128
+ }
129
+ }
130
+ }
131
+
132
+ if (report.summary.scanErrors > 0) {
133
+ lines.push(`trailmark: scan errors=${report.summary.scanErrors}`)
134
+ }
135
+
136
+ return lines.join("\n")
137
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./types"
2
+ export * from "./buildReport"
3
+ export * from "./formatConsole"
@@ -0,0 +1,28 @@
1
+ import { impactRank } from "../impact"
2
+ import { TrailmarkImpact } from "../types"
3
+
4
+ export interface IssueSortInput {
5
+ issueId: string
6
+ ruleId: string
7
+ pageKey: string
8
+ impact?: TrailmarkImpact
9
+ }
10
+
11
+ export function compareIssueLike(left: IssueSortInput, right: IssueSortInput): number {
12
+ const impactDiff = impactRank(left.impact) - impactRank(right.impact)
13
+ if (impactDiff !== 0) {
14
+ return impactDiff
15
+ }
16
+
17
+ const ruleDiff = left.ruleId.localeCompare(right.ruleId)
18
+ if (ruleDiff !== 0) {
19
+ return ruleDiff
20
+ }
21
+
22
+ const pageDiff = left.pageKey.localeCompare(right.pageKey)
23
+ if (pageDiff !== 0) {
24
+ return pageDiff
25
+ }
26
+
27
+ return left.issueId.localeCompare(right.issueId)
28
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ DiffResult,
3
+ TrailmarkAttachment,
4
+ TrailmarkImpact,
5
+ TrailmarkRun,
6
+ TrailmarkToolInfo,
7
+ } from "../types"
8
+
9
+ export type TrailmarkReportMode = "baseline" | "check"
10
+
11
+ export interface TrailmarkArtifactIndex {
12
+ byOccurrenceId?: Record<string, TrailmarkAttachment[]>
13
+ byIssueId?: Record<string, TrailmarkAttachment[]>
14
+ }
15
+
16
+ export interface TrailmarkReportOccurrence {
17
+ occurrenceId?: string
18
+ testId?: string
19
+ projectName?: string
20
+ url: string
21
+ timestamp: string
22
+ attachments: TrailmarkAttachment[]
23
+ }
24
+
25
+ export interface TrailmarkReportIssue {
26
+ issueId: string
27
+ ruleId: string
28
+ impact?: TrailmarkImpact
29
+ tags: string[]
30
+ pageKey: string
31
+ url: string
32
+ target: string[]
33
+ selectorHint?: string
34
+ failureSummary?: string
35
+ occurrences: TrailmarkReportOccurrence[]
36
+ }
37
+
38
+ export interface TrailmarkReportDiffIssueSummary {
39
+ issueId: string
40
+ ruleId: string
41
+ impact?: TrailmarkImpact
42
+ pageKey: string
43
+ selectorHint?: string
44
+ occurrenceCount: number
45
+ }
46
+
47
+ export interface TrailmarkReportDiff {
48
+ newIssueIds: string[]
49
+ existingIssueIds: string[]
50
+ resolvedIssueIds: string[]
51
+ newIssues: TrailmarkReportDiffIssueSummary[]
52
+ }
53
+
54
+ export interface TrailmarkReportSummary {
55
+ scannedPages: number
56
+ totalIssues: number
57
+ totalOccurrences: number
58
+ scanErrors: number
59
+ newIssues: number
60
+ existingIssues: number
61
+ resolvedIssues: number
62
+ byImpact: Record<string, number>
63
+ byRuleId: Record<string, number>
64
+ byPageKey: Record<string, number>
65
+ }
66
+
67
+ export interface TrailmarkReportRunMetadata {
68
+ reportVersion: 2
69
+ mode: TrailmarkReportMode
70
+ createdAt: string
71
+ startedAt: string
72
+ finishedAt?: string
73
+ configHash: string
74
+ tool: TrailmarkToolInfo
75
+ engine: {
76
+ scanner: string
77
+ runtime: "playwright"
78
+ }
79
+ metadata?: Record<string, unknown>
80
+ }
81
+
82
+ export interface TrailmarkReport {
83
+ run: TrailmarkReportRunMetadata
84
+ summary: TrailmarkReportSummary
85
+ issues: TrailmarkReportIssue[]
86
+ diff?: TrailmarkReportDiff
87
+ }
88
+
89
+ export interface BuildReportInput {
90
+ run: TrailmarkRun
91
+ mode: TrailmarkReportMode
92
+ configHash: string
93
+ diff?: DiffResult
94
+ createdAt?: string
95
+ artifacts?: TrailmarkArtifactIndex
96
+ }
97
+
98
+ export interface FormatConsoleOptions {
99
+ failOnImpact?: string[]
100
+ topPagesPerRule?: number
101
+ }
package/src/run.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { TrailmarkRun, TrailmarkToolInfo } from "./types"
2
+
3
+ export const DEFAULT_TOOL_INFO: TrailmarkToolInfo = {
4
+ name: "trailmark",
5
+ version: "0.1.0",
6
+ scanner: "@axe-core/playwright",
7
+ }
8
+
9
+ export function createEmptyRun(options?: {
10
+ metadata?: Record<string, unknown>
11
+ tool?: TrailmarkToolInfo
12
+ startedAt?: string
13
+ }): TrailmarkRun {
14
+ return {
15
+ version: 1,
16
+ startedAt: options?.startedAt ?? new Date().toISOString(),
17
+ tool: options?.tool ?? DEFAULT_TOOL_INFO,
18
+ findings: [],
19
+ scanErrors: [],
20
+ metadata: options?.metadata,
21
+ }
22
+ }
package/src/scope.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { BaselineEntry, TrailmarkFindingMeta } from "./types"
2
+
3
+ const DEFAULT_SCOPE_KEY = "default"
4
+
5
+ function normalizeScopeValue(value: string | undefined): string {
6
+ const normalized = value?.trim().toLowerCase()
7
+ if (!normalized) {
8
+ return DEFAULT_SCOPE_KEY
9
+ }
10
+ return normalized
11
+ }
12
+
13
+ export function scopeKeyFromMeta(meta: TrailmarkFindingMeta | undefined): string {
14
+ return normalizeScopeValue(meta?.projectName)
15
+ }
16
+
17
+ export function scopeKeyFromBaselineEntry(
18
+ entry: Pick<BaselineEntry, "scopeKey" | "projectName">,
19
+ ): string {
20
+ return normalizeScopeValue(entry.scopeKey ?? entry.projectName)
21
+ }
22
+
23
+ export function scopedIssueKey(issueId: string, scopeKey: string): string {
24
+ return `${scopeKey}::${issueId}`
25
+ }