@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 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/report/types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,iBAAiB,EAClB,MAAM,UAAU,CAAA;AAEjB,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,OAAO,CAAA;AAEtD,MAAM,WAAW,sBAAsB;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAA;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAA;CAClD;AAED,MAAM,WAAW,yBAAyB;IACxC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,mBAAmB,EAAE,CAAA;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,yBAAyB,EAAE,CAAA;CACzC;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,SAAS,EAAE,+BAA+B,EAAE,CAAA;CAC7C;AAED,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,0BAA0B;IACzC,aAAa,EAAE,CAAC,CAAA;IAChB,IAAI,EAAE,mBAAmB,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,iBAAiB,CAAA;IACvB,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,YAAY,CAAA;KACtB,CAAA;IACD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,0BAA0B,CAAA;IAC/B,OAAO,EAAE,sBAAsB,CAAA;IAC/B,MAAM,EAAE,oBAAoB,EAAE,CAAA;IAC9B,IAAI,CAAC,EAAE,mBAAmB,CAAA;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,YAAY,CAAA;IACjB,IAAI,EAAE,mBAAmB,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,sBAAsB,CAAA;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/report/types.ts"],"names":[],"mappings":""}
package/dist/run.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { TrailmarkRun, TrailmarkToolInfo } from "./types";
2
+ export declare const DEFAULT_TOOL_INFO: TrailmarkToolInfo;
3
+ export declare function createEmptyRun(options?: {
4
+ metadata?: Record<string, unknown>;
5
+ tool?: TrailmarkToolInfo;
6
+ startedAt?: string;
7
+ }): TrailmarkRun;
8
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAEzD,eAAO,MAAM,iBAAiB,EAAE,iBAI/B,CAAA;AAED,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,YAAY,CASf"}
package/dist/run.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_TOOL_INFO = void 0;
4
+ exports.createEmptyRun = createEmptyRun;
5
+ exports.DEFAULT_TOOL_INFO = {
6
+ name: "trailmark",
7
+ version: "0.1.0",
8
+ scanner: "@axe-core/playwright",
9
+ };
10
+ function createEmptyRun(options) {
11
+ return {
12
+ version: 1,
13
+ startedAt: options?.startedAt ?? new Date().toISOString(),
14
+ tool: options?.tool ?? exports.DEFAULT_TOOL_INFO,
15
+ findings: [],
16
+ scanErrors: [],
17
+ metadata: options?.metadata,
18
+ };
19
+ }
20
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":";;;AAQA,wCAaC;AAnBY,QAAA,iBAAiB,GAAsB;IAClD,IAAI,EAAE,WAAW;IACjB,OAAO,EAAE,OAAO;IAChB,OAAO,EAAE,sBAAsB;CAChC,CAAA;AAED,SAAgB,cAAc,CAAC,OAI9B;IACC,OAAO;QACL,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,OAAO,EAAE,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACzD,IAAI,EAAE,OAAO,EAAE,IAAI,IAAI,yBAAiB;QACxC,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,EAAE;QACd,QAAQ,EAAE,OAAO,EAAE,QAAQ;KAC5B,CAAA;AACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { BaselineEntry, TrailmarkFindingMeta } from "./types";
2
+ export declare function scopeKeyFromMeta(meta: TrailmarkFindingMeta | undefined): string;
3
+ export declare function scopeKeyFromBaselineEntry(entry: Pick<BaselineEntry, "scopeKey" | "projectName">): string;
4
+ export declare function scopedIssueKey(issueId: string, scopeKey: string): string;
5
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAY7D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,SAAS,GAAG,MAAM,CAE/E;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,GAAG,aAAa,CAAC,GACrD,MAAM,CAER;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExE"}
package/dist/scope.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scopeKeyFromMeta = scopeKeyFromMeta;
4
+ exports.scopeKeyFromBaselineEntry = scopeKeyFromBaselineEntry;
5
+ exports.scopedIssueKey = scopedIssueKey;
6
+ const DEFAULT_SCOPE_KEY = "default";
7
+ function normalizeScopeValue(value) {
8
+ const normalized = value?.trim().toLowerCase();
9
+ if (!normalized) {
10
+ return DEFAULT_SCOPE_KEY;
11
+ }
12
+ return normalized;
13
+ }
14
+ function scopeKeyFromMeta(meta) {
15
+ return normalizeScopeValue(meta?.projectName);
16
+ }
17
+ function scopeKeyFromBaselineEntry(entry) {
18
+ return normalizeScopeValue(entry.scopeKey ?? entry.projectName);
19
+ }
20
+ function scopedIssueKey(issueId, scopeKey) {
21
+ return `${scopeKey}::${issueId}`;
22
+ }
23
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":";;AAYA,4CAEC;AAED,8DAIC;AAED,wCAEC;AAtBD,MAAM,iBAAiB,GAAG,SAAS,CAAA;AAEnC,SAAS,mBAAmB,CAAC,KAAyB;IACpD,MAAM,UAAU,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAC9C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,iBAAiB,CAAA;IAC1B,CAAC;IACD,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,SAAgB,gBAAgB,CAAC,IAAsC;IACrE,OAAO,mBAAmB,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;AAC/C,CAAC;AAED,SAAgB,yBAAyB,CACvC,KAAsD;IAEtD,OAAO,mBAAmB,CAAC,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,WAAW,CAAC,CAAA;AACjE,CAAC;AAED,SAAgB,cAAc,CAAC,OAAe,EAAE,QAAgB;IAC9D,OAAO,GAAG,QAAQ,KAAK,OAAO,EAAE,CAAA;AAClC,CAAC"}
@@ -0,0 +1,150 @@
1
+ export type TrailmarkImpactKey = "minor" | "moderate" | "serious" | "critical" | "unknown";
2
+ export type TrailmarkImpact = "minor" | "moderate" | "serious" | "critical" | null | undefined;
3
+ export interface UrlRoutePattern {
4
+ match: string;
5
+ replace: string;
6
+ }
7
+ export interface UrlNormalizationOptions {
8
+ stripHash?: boolean;
9
+ stripQuery?: boolean | {
10
+ allow: string[];
11
+ };
12
+ routePatterns?: UrlRoutePattern[];
13
+ }
14
+ export interface TrailmarkFindingMeta {
15
+ testId?: string;
16
+ testTitle?: string;
17
+ testFile?: string;
18
+ projectName?: string;
19
+ scanLabel?: string;
20
+ }
21
+ export interface TrailmarkScanError {
22
+ pageUrl?: string;
23
+ pageKey?: string;
24
+ message: string;
25
+ stack?: string;
26
+ occurredAt: string;
27
+ meta?: TrailmarkFindingMeta;
28
+ }
29
+ export interface TrailmarkAttachment {
30
+ kind: string;
31
+ path?: string;
32
+ error?: string;
33
+ }
34
+ export interface TrailmarkFinding {
35
+ issueId: string;
36
+ relaxedIssueId: string;
37
+ ruleId: string;
38
+ impact?: TrailmarkImpact;
39
+ tags: string[];
40
+ help?: string;
41
+ helpUrl?: string;
42
+ pageUrl: string;
43
+ pageKey: string;
44
+ target: string[];
45
+ selectorHint?: string;
46
+ targetHash: string;
47
+ nodeHtml?: string;
48
+ failureSummary?: string;
49
+ failureHint?: string;
50
+ occurredAt?: string;
51
+ occurrenceId?: string;
52
+ attachments?: TrailmarkAttachment[];
53
+ meta?: TrailmarkFindingMeta;
54
+ }
55
+ export interface TrailmarkToolInfo {
56
+ name: string;
57
+ version: string;
58
+ scanner: string;
59
+ }
60
+ export interface TrailmarkRun {
61
+ version: 1;
62
+ startedAt: string;
63
+ finishedAt?: string;
64
+ tool: TrailmarkToolInfo;
65
+ findings: TrailmarkFinding[];
66
+ scanErrors: TrailmarkScanError[];
67
+ metadata?: Record<string, unknown>;
68
+ }
69
+ export interface BaselineEntry {
70
+ issueId: string;
71
+ scopeKey?: string;
72
+ projectName?: string;
73
+ relaxedIssueId: string;
74
+ ruleId: string;
75
+ impact?: TrailmarkImpact;
76
+ pageKey: string;
77
+ pageUrl: string;
78
+ targetHash: string;
79
+ selectorHint?: string;
80
+ sampleTarget?: string;
81
+ help?: string;
82
+ helpUrl?: string;
83
+ tags: string[];
84
+ failureHint?: string;
85
+ occurrences: number;
86
+ firstSeenAt: string;
87
+ lastSeenAt: string;
88
+ }
89
+ export interface BaselineFile {
90
+ version: 1;
91
+ createdAt: string;
92
+ tool: TrailmarkToolInfo;
93
+ configHash: string;
94
+ findings: Record<string, BaselineEntry>;
95
+ }
96
+ export interface DiffMatch {
97
+ finding: TrailmarkFinding;
98
+ matchedBy: "exact" | "relaxed";
99
+ baselineIssueId: string;
100
+ baselineScopeKey?: string;
101
+ }
102
+ export interface DiffStats {
103
+ totalCurrent: number;
104
+ totalBaseline: number;
105
+ newCount: number;
106
+ existingCount: number;
107
+ resolvedCount: number;
108
+ newByImpact: Record<string, number>;
109
+ }
110
+ export interface DiffResult {
111
+ newFindings: TrailmarkFinding[];
112
+ existing: DiffMatch[];
113
+ resolved: BaselineEntry[];
114
+ blockingNew: TrailmarkFinding[];
115
+ stats: DiffStats;
116
+ }
117
+ export interface DiffOptions {
118
+ failOnImpact?: string[];
119
+ onDebug?: (message: string, context?: Record<string, unknown>) => void;
120
+ }
121
+ export interface AxeNodeResult {
122
+ target: string[];
123
+ html?: string;
124
+ failureSummary?: string;
125
+ }
126
+ export interface AxeViolation {
127
+ id: string;
128
+ impact?: TrailmarkImpact;
129
+ tags?: string[];
130
+ help?: string;
131
+ helpUrl?: string;
132
+ nodes: AxeNodeResult[];
133
+ }
134
+ export interface AxeLikeResults {
135
+ violations: AxeViolation[];
136
+ }
137
+ export interface NormalizeInput {
138
+ results: AxeLikeResults;
139
+ pageUrl: string;
140
+ urlOptions?: UrlNormalizationOptions;
141
+ meta?: TrailmarkFindingMeta;
142
+ htmlMaxLength?: number;
143
+ failureSummaryMaxLength?: number;
144
+ }
145
+ export interface TargetFingerprintCanonical {
146
+ tag: string | null;
147
+ stableAttrs: Record<string, string>;
148
+ structuralHint: string | null;
149
+ }
150
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/dist/url.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { UrlNormalizationOptions } from "./types";
2
+ export declare function normalizeUrlToPageKey(rawUrl: string, options?: UrlNormalizationOptions): string;
3
+ //# sourceMappingURL=url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAqEjD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,uBAA4B,GACpC,MAAM,CA6BR"}
package/dist/url.js ADDED
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeUrlToPageKey = normalizeUrlToPageKey;
4
+ const DEFAULT_OPTIONS = {
5
+ stripHash: true,
6
+ stripQuery: true,
7
+ };
8
+ function normalizeQuery(params, allow) {
9
+ const keys = Array.from(new Set(params.keys()))
10
+ .filter((key) => !allow || allow.has(key))
11
+ .sort((a, b) => a.localeCompare(b));
12
+ const pairs = [];
13
+ for (const key of keys) {
14
+ const values = params.getAll(key).sort((a, b) => a.localeCompare(b));
15
+ for (const value of values) {
16
+ pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
17
+ }
18
+ }
19
+ return pairs.join("&");
20
+ }
21
+ function stripQueryFromFallback(raw, stripQuery) {
22
+ if (stripQuery === true) {
23
+ return raw.replace(/\?.*$/, "");
24
+ }
25
+ if (stripQuery && typeof stripQuery === "object") {
26
+ const [base, rawQuery = ""] = raw.split("?");
27
+ if (!rawQuery) {
28
+ return base;
29
+ }
30
+ const allow = new Set(stripQuery.allow);
31
+ const params = new URLSearchParams(rawQuery);
32
+ const query = normalizeQuery(params, allow);
33
+ return query ? `${base}?${query}` : base;
34
+ }
35
+ return raw;
36
+ }
37
+ function applyRoutePatterns(pageKey, routePatterns) {
38
+ if (!routePatterns || routePatterns.length === 0) {
39
+ return pageKey;
40
+ }
41
+ let result = pageKey;
42
+ for (const pattern of routePatterns) {
43
+ try {
44
+ const matcher = new RegExp(pattern.match);
45
+ if (matcher.test(result)) {
46
+ result = result.replace(matcher, pattern.replace);
47
+ }
48
+ }
49
+ catch {
50
+ continue;
51
+ }
52
+ }
53
+ return result;
54
+ }
55
+ function normalizeUrlToPageKey(rawUrl, options = {}) {
56
+ const merged = {
57
+ ...DEFAULT_OPTIONS,
58
+ ...options,
59
+ };
60
+ let pageKey;
61
+ try {
62
+ const parsed = new URL(rawUrl);
63
+ const host = parsed.host.toLowerCase();
64
+ const pathname = parsed.pathname || "/";
65
+ let query = "";
66
+ if (merged.stripQuery === false) {
67
+ query = normalizeQuery(parsed.searchParams);
68
+ }
69
+ else if (merged.stripQuery && typeof merged.stripQuery === "object") {
70
+ query = normalizeQuery(parsed.searchParams, new Set(merged.stripQuery.allow));
71
+ }
72
+ const hash = !merged.stripHash ? parsed.hash : "";
73
+ pageKey = `${host}${pathname}${query ? `?${query}` : ""}${hash}`;
74
+ }
75
+ catch {
76
+ const lowered = rawUrl.trim().toLowerCase();
77
+ const withoutHash = merged.stripHash ? lowered.replace(/#.*$/, "") : lowered;
78
+ pageKey = stripQueryFromFallback(withoutHash, merged.stripQuery);
79
+ }
80
+ return applyRoutePatterns(pageKey, merged.routePatterns);
81
+ }
82
+ //# sourceMappingURL=url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.js","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@trailmark/core",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "types": "src/index.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "dependencies": {
13
+ "zod": "^3.24.1"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^22.10.5"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "test": "vitest run",
21
+ "typecheck": "tsc -p tsconfig.json --noEmit"
22
+ }
23
+ }
@@ -0,0 +1,217 @@
1
+ import { hashObject } from "./hash"
2
+ import { IMPACT_ORDER } from "./impact"
3
+ import { DEFAULT_TOOL_INFO } from "./run"
4
+ import { scopedIssueKey, scopeKeyFromMeta } from "./scope"
5
+ import { BaselineEntry, BaselineFile, TrailmarkImpact, TrailmarkRun } from "./types"
6
+ import { z } from "zod"
7
+
8
+ export const BASELINE_SCHEMA_VERSION = 1
9
+
10
+ export class BaselineFormatError extends Error {
11
+ constructor(message: string) {
12
+ super(message)
13
+ this.name = "BaselineFormatError"
14
+ }
15
+ }
16
+
17
+ export function computeConfigHash(config: unknown): string {
18
+ return hashObject(config)
19
+ }
20
+
21
+ function isRecord(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value)
23
+ }
24
+
25
+ function normalizeTags(tags: string[]): string[] {
26
+ return Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b))
27
+ }
28
+
29
+ const baselineEntrySchema: z.ZodType<BaselineEntry> = z.object({
30
+ issueId: z.string(),
31
+ scopeKey: z.string().optional(),
32
+ projectName: z.string().optional(),
33
+ relaxedIssueId: z.string(),
34
+ ruleId: z.string(),
35
+ impact: z
36
+ .enum(
37
+ IMPACT_ORDER.filter((value) => value !== "unknown") as [
38
+ NonNullable<TrailmarkImpact>,
39
+ ...NonNullable<TrailmarkImpact>[],
40
+ ],
41
+ )
42
+ .nullable()
43
+ .optional(),
44
+ pageKey: z.string(),
45
+ pageUrl: z.string(),
46
+ targetHash: z.string(),
47
+ selectorHint: z.string().optional(),
48
+ sampleTarget: z.string().optional(),
49
+ help: z.string().optional(),
50
+ helpUrl: z.string().optional(),
51
+ tags: z.array(z.string()).transform((tags) => normalizeTags(tags)),
52
+ failureHint: z.string().optional(),
53
+ occurrences: z.number().finite().min(0),
54
+ firstSeenAt: z.string(),
55
+ lastSeenAt: z.string(),
56
+ })
57
+
58
+ const baselineFileSchema: z.ZodType<BaselineFile> = z.object({
59
+ version: z.literal(BASELINE_SCHEMA_VERSION),
60
+ createdAt: z.string(),
61
+ tool: z.object({
62
+ name: z.string(),
63
+ version: z.string(),
64
+ scanner: z.string(),
65
+ }),
66
+ configHash: z.string(),
67
+ findings: z.record(z.string(), baselineEntrySchema),
68
+ })
69
+
70
+ function formatPath(path: (string | number)[]): string {
71
+ if (path.length === 0) {
72
+ return "baseline"
73
+ }
74
+
75
+ return path
76
+ .map((segment) => (typeof segment === "number" ? `[${String(segment)}]` : segment))
77
+ .join(".")
78
+ }
79
+
80
+ function comparableEntry(entry: BaselineEntry): Record<string, unknown> {
81
+ return {
82
+ issueId: entry.issueId,
83
+ scopeKey: entry.scopeKey,
84
+ projectName: entry.projectName,
85
+ relaxedIssueId: entry.relaxedIssueId,
86
+ ruleId: entry.ruleId,
87
+ impact: entry.impact,
88
+ pageKey: entry.pageKey,
89
+ pageUrl: entry.pageUrl,
90
+ targetHash: entry.targetHash,
91
+ selectorHint: entry.selectorHint,
92
+ sampleTarget: entry.sampleTarget,
93
+ help: entry.help,
94
+ helpUrl: entry.helpUrl,
95
+ tags: entry.tags,
96
+ failureHint: entry.failureHint,
97
+ occurrences: entry.occurrences,
98
+ }
99
+ }
100
+
101
+ function isUnchangedEntry(previous: BaselineEntry, next: BaselineEntry): boolean {
102
+ return hashObject(comparableEntry(previous)) === hashObject(comparableEntry(next))
103
+ }
104
+
105
+ export function parseBaselineFile(raw: string) {
106
+ let parsed: unknown
107
+ try {
108
+ parsed = JSON.parse(raw)
109
+ } catch {
110
+ return {
111
+ data: null,
112
+ error: true as const,
113
+ message: "trailmark: baseline file is not a valid JSON.",
114
+ }
115
+ }
116
+
117
+ if (
118
+ isRecord(parsed) &&
119
+ typeof parsed.version === "number" &&
120
+ parsed.version !== BASELINE_SCHEMA_VERSION
121
+ ) {
122
+ return {
123
+ data: null,
124
+ error: true as const,
125
+ message: `trailmark: unsupported baseline version ${parsed.version}. Expected ${BASELINE_SCHEMA_VERSION}.`,
126
+ }
127
+ }
128
+
129
+ const result = baselineFileSchema.safeParse(parsed)
130
+ if (!result.success) {
131
+ const issue = result.error.issues[0]
132
+ return {
133
+ data: null,
134
+ error: true as const,
135
+ message: `trailmark: invalid baseline format at "${formatPath(issue.path)}": ${issue.message}`,
136
+ }
137
+ }
138
+
139
+ return {
140
+ error: false as const,
141
+ data: result.data,
142
+ }
143
+ }
144
+
145
+ export function buildBaselineFromRun(
146
+ run: TrailmarkRun,
147
+ options: {
148
+ createdAt?: string
149
+ configHash: string
150
+ previousBaseline?: BaselineFile
151
+ },
152
+ ): BaselineFile {
153
+ const createdAt =
154
+ options.createdAt ?? options.previousBaseline?.createdAt ?? new Date().toISOString()
155
+ const previousFindings = options.previousBaseline?.findings ?? {}
156
+ const findings: Record<string, BaselineEntry> = {}
157
+
158
+ const sortedFindings = [...run.findings].sort((a, b) => {
159
+ const aKey = scopedIssueKey(a.issueId, scopeKeyFromMeta(a.meta))
160
+ const bKey = scopedIssueKey(b.issueId, scopeKeyFromMeta(b.meta))
161
+ return aKey.localeCompare(bKey)
162
+ })
163
+
164
+ for (const finding of sortedFindings) {
165
+ const scopeKey = scopeKeyFromMeta(finding.meta)
166
+ const entryKey = scopedIssueKey(finding.issueId, scopeKey)
167
+ const existing = findings[entryKey]
168
+ if (existing) {
169
+ existing.occurrences += 1
170
+ continue
171
+ }
172
+
173
+ const candidate: BaselineEntry = {
174
+ issueId: finding.issueId,
175
+ scopeKey,
176
+ projectName: finding.meta?.projectName,
177
+ relaxedIssueId: finding.relaxedIssueId,
178
+ ruleId: finding.ruleId,
179
+ impact: finding.impact,
180
+ pageKey: finding.pageKey,
181
+ pageUrl: finding.pageUrl,
182
+ targetHash: finding.targetHash,
183
+ selectorHint: finding.selectorHint,
184
+ sampleTarget: finding.target[0],
185
+ help: finding.help,
186
+ helpUrl: finding.helpUrl,
187
+ tags: normalizeTags([...finding.tags]),
188
+ failureHint: finding.failureHint,
189
+ occurrences: 1,
190
+ firstSeenAt: createdAt,
191
+ lastSeenAt: createdAt,
192
+ }
193
+
194
+ const previous = previousFindings[entryKey]
195
+ if (previous) {
196
+ candidate.firstSeenAt = previous.firstSeenAt
197
+ if (isUnchangedEntry(previous, candidate)) {
198
+ candidate.lastSeenAt = previous.lastSeenAt
199
+ }
200
+ }
201
+
202
+ findings[entryKey] = candidate
203
+ }
204
+
205
+ const orderedFindings: Record<string, BaselineEntry> = {}
206
+ for (const key of Object.keys(findings).sort((a, b) => a.localeCompare(b))) {
207
+ orderedFindings[key] = findings[key]
208
+ }
209
+
210
+ return {
211
+ version: BASELINE_SCHEMA_VERSION,
212
+ createdAt,
213
+ tool: run.tool ?? DEFAULT_TOOL_INFO,
214
+ configHash: options.configHash,
215
+ findings: orderedFindings,
216
+ }
217
+ }