@vertaaux/cli 0.2.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 (198) hide show
  1. package/README.md +345 -0
  2. package/dist/auth/ci-token.d.ts +49 -0
  3. package/dist/auth/ci-token.d.ts.map +1 -0
  4. package/dist/auth/ci-token.js +83 -0
  5. package/dist/auth/device-flow.d.ts +66 -0
  6. package/dist/auth/device-flow.d.ts.map +1 -0
  7. package/dist/auth/device-flow.js +156 -0
  8. package/dist/auth/token-store.d.ts +53 -0
  9. package/dist/auth/token-store.d.ts.map +1 -0
  10. package/dist/auth/token-store.js +78 -0
  11. package/dist/baseline/diff.d.ts +57 -0
  12. package/dist/baseline/diff.d.ts.map +1 -0
  13. package/dist/baseline/diff.js +152 -0
  14. package/dist/baseline/hash.d.ts +54 -0
  15. package/dist/baseline/hash.d.ts.map +1 -0
  16. package/dist/baseline/hash.js +66 -0
  17. package/dist/baseline/manager.d.ts +89 -0
  18. package/dist/baseline/manager.d.ts.map +1 -0
  19. package/dist/baseline/manager.js +157 -0
  20. package/dist/cache/index.d.ts +8 -0
  21. package/dist/cache/index.d.ts.map +1 -0
  22. package/dist/cache/index.js +7 -0
  23. package/dist/cache/route-cache.d.ts +119 -0
  24. package/dist/cache/route-cache.d.ts.map +1 -0
  25. package/dist/cache/route-cache.js +213 -0
  26. package/dist/ci/changed-routes.d.ts +95 -0
  27. package/dist/ci/changed-routes.d.ts.map +1 -0
  28. package/dist/ci/changed-routes.js +304 -0
  29. package/dist/ci/github-api.d.ts +68 -0
  30. package/dist/ci/github-api.d.ts.map +1 -0
  31. package/dist/ci/github-api.js +138 -0
  32. package/dist/ci/gitlab-api.d.ts +75 -0
  33. package/dist/ci/gitlab-api.d.ts.map +1 -0
  34. package/dist/ci/gitlab-api.js +180 -0
  35. package/dist/ci/index.d.ts +6 -0
  36. package/dist/ci/index.d.ts.map +1 -0
  37. package/dist/ci/index.js +4 -0
  38. package/dist/commands/audit.d.ts +58 -0
  39. package/dist/commands/audit.d.ts.map +1 -0
  40. package/dist/commands/audit.js +862 -0
  41. package/dist/commands/baseline.d.ts +22 -0
  42. package/dist/commands/baseline.d.ts.map +1 -0
  43. package/dist/commands/baseline.js +210 -0
  44. package/dist/commands/comment.d.ts +14 -0
  45. package/dist/commands/comment.d.ts.map +1 -0
  46. package/dist/commands/comment.js +363 -0
  47. package/dist/commands/diff.d.ts +24 -0
  48. package/dist/commands/diff.d.ts.map +1 -0
  49. package/dist/commands/diff.js +196 -0
  50. package/dist/commands/doctor.d.ts +58 -0
  51. package/dist/commands/doctor.d.ts.map +1 -0
  52. package/dist/commands/doctor.js +338 -0
  53. package/dist/commands/download.d.ts +12 -0
  54. package/dist/commands/download.d.ts.map +1 -0
  55. package/dist/commands/download.js +183 -0
  56. package/dist/commands/explain.d.ts +62 -0
  57. package/dist/commands/explain.d.ts.map +1 -0
  58. package/dist/commands/explain.js +302 -0
  59. package/dist/commands/init.d.ts +12 -0
  60. package/dist/commands/init.d.ts.map +1 -0
  61. package/dist/commands/init.js +212 -0
  62. package/dist/commands/login.d.ts +14 -0
  63. package/dist/commands/login.d.ts.map +1 -0
  64. package/dist/commands/login.js +222 -0
  65. package/dist/commands/policy.d.ts +13 -0
  66. package/dist/commands/policy.d.ts.map +1 -0
  67. package/dist/commands/policy.js +347 -0
  68. package/dist/commands/upload.d.ts +12 -0
  69. package/dist/commands/upload.d.ts.map +1 -0
  70. package/dist/commands/upload.js +158 -0
  71. package/dist/config/defaults.d.ts +21 -0
  72. package/dist/config/defaults.d.ts.map +1 -0
  73. package/dist/config/defaults.js +49 -0
  74. package/dist/config/loader.d.ts +66 -0
  75. package/dist/config/loader.d.ts.map +1 -0
  76. package/dist/config/loader.js +167 -0
  77. package/dist/config/schema.d.ts +55 -0
  78. package/dist/config/schema.d.ts.map +1 -0
  79. package/dist/config/schema.js +6 -0
  80. package/dist/index.d.ts +9 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +1090 -0
  83. package/dist/interactive/fix-wizard.d.ts +44 -0
  84. package/dist/interactive/fix-wizard.d.ts.map +1 -0
  85. package/dist/interactive/fix-wizard.js +286 -0
  86. package/dist/interactive/init-wizard.d.ts +32 -0
  87. package/dist/interactive/init-wizard.d.ts.map +1 -0
  88. package/dist/interactive/init-wizard.js +193 -0
  89. package/dist/interactive/prompts.d.ts +62 -0
  90. package/dist/interactive/prompts.d.ts.map +1 -0
  91. package/dist/interactive/prompts.js +78 -0
  92. package/dist/monorepo/detector.d.ts +70 -0
  93. package/dist/monorepo/detector.d.ts.map +1 -0
  94. package/dist/monorepo/detector.js +278 -0
  95. package/dist/monorepo/index.d.ts +9 -0
  96. package/dist/monorepo/index.d.ts.map +1 -0
  97. package/dist/monorepo/index.js +8 -0
  98. package/dist/monorepo/workspace.d.ts +142 -0
  99. package/dist/monorepo/workspace.d.ts.map +1 -0
  100. package/dist/monorepo/workspace.js +171 -0
  101. package/dist/output/envelope.d.ts +21 -0
  102. package/dist/output/envelope.d.ts.map +1 -0
  103. package/dist/output/envelope.js +27 -0
  104. package/dist/output/factory.d.ts +73 -0
  105. package/dist/output/factory.d.ts.map +1 -0
  106. package/dist/output/factory.js +60 -0
  107. package/dist/output/formats.d.ts +11 -0
  108. package/dist/output/formats.d.ts.map +1 -0
  109. package/dist/output/formats.js +41 -0
  110. package/dist/output/html.d.ts +45 -0
  111. package/dist/output/html.d.ts.map +1 -0
  112. package/dist/output/html.js +607 -0
  113. package/dist/output/human.d.ts +41 -0
  114. package/dist/output/human.d.ts.map +1 -0
  115. package/dist/output/human.js +274 -0
  116. package/dist/output/json.d.ts +42 -0
  117. package/dist/output/json.d.ts.map +1 -0
  118. package/dist/output/json.js +37 -0
  119. package/dist/output/junit.d.ts +56 -0
  120. package/dist/output/junit.d.ts.map +1 -0
  121. package/dist/output/junit.js +135 -0
  122. package/dist/output/markdown.d.ts +77 -0
  123. package/dist/output/markdown.d.ts.map +1 -0
  124. package/dist/output/markdown.js +411 -0
  125. package/dist/output/sarif.d.ts +160 -0
  126. package/dist/output/sarif.d.ts.map +1 -0
  127. package/dist/output/sarif.js +207 -0
  128. package/dist/policy/evaluator.d.ts +111 -0
  129. package/dist/policy/evaluator.d.ts.map +1 -0
  130. package/dist/policy/evaluator.js +362 -0
  131. package/dist/policy/index.d.ts +15 -0
  132. package/dist/policy/index.d.ts.map +1 -0
  133. package/dist/policy/index.js +11 -0
  134. package/dist/policy/loader.d.ts +97 -0
  135. package/dist/policy/loader.d.ts.map +1 -0
  136. package/dist/policy/loader.js +281 -0
  137. package/dist/policy/schema.d.ts +297 -0
  138. package/dist/policy/schema.d.ts.map +1 -0
  139. package/dist/policy/schema.js +230 -0
  140. package/dist/quality-gate/evaluator.d.ts +58 -0
  141. package/dist/quality-gate/evaluator.d.ts.map +1 -0
  142. package/dist/quality-gate/evaluator.js +274 -0
  143. package/dist/quality-gate/index.d.ts +10 -0
  144. package/dist/quality-gate/index.d.ts.map +1 -0
  145. package/dist/quality-gate/index.js +7 -0
  146. package/dist/quality-gate/types.d.ts +103 -0
  147. package/dist/quality-gate/types.d.ts.map +1 -0
  148. package/dist/quality-gate/types.js +23 -0
  149. package/dist/templates/azure-devops.d.ts +25 -0
  150. package/dist/templates/azure-devops.d.ts.map +1 -0
  151. package/dist/templates/azure-devops.js +109 -0
  152. package/dist/templates/circleci.d.ts +28 -0
  153. package/dist/templates/circleci.d.ts.map +1 -0
  154. package/dist/templates/circleci.js +86 -0
  155. package/dist/templates/github-actions.d.ts +81 -0
  156. package/dist/templates/github-actions.d.ts.map +1 -0
  157. package/dist/templates/github-actions.js +393 -0
  158. package/dist/templates/gitlab-ci.d.ts +26 -0
  159. package/dist/templates/gitlab-ci.d.ts.map +1 -0
  160. package/dist/templates/gitlab-ci.js +70 -0
  161. package/dist/templates/index.d.ts +72 -0
  162. package/dist/templates/index.d.ts.map +1 -0
  163. package/dist/templates/index.js +112 -0
  164. package/dist/templates/jenkins.d.ts +26 -0
  165. package/dist/templates/jenkins.d.ts.map +1 -0
  166. package/dist/templates/jenkins.js +110 -0
  167. package/dist/ui/banner.d.ts +31 -0
  168. package/dist/ui/banner.d.ts.map +1 -0
  169. package/dist/ui/banner.js +84 -0
  170. package/dist/ui/diagnostics.d.ts +39 -0
  171. package/dist/ui/diagnostics.d.ts.map +1 -0
  172. package/dist/ui/diagnostics.js +153 -0
  173. package/dist/ui/spinner.d.ts +61 -0
  174. package/dist/ui/spinner.d.ts.map +1 -0
  175. package/dist/ui/spinner.js +101 -0
  176. package/dist/ui/table.d.ts +63 -0
  177. package/dist/ui/table.d.ts.map +1 -0
  178. package/dist/ui/table.js +236 -0
  179. package/dist/utils/client.d.ts +82 -0
  180. package/dist/utils/client.d.ts.map +1 -0
  181. package/dist/utils/client.js +128 -0
  182. package/dist/utils/detect-env.d.ts +59 -0
  183. package/dist/utils/detect-env.d.ts.map +1 -0
  184. package/dist/utils/detect-env.js +115 -0
  185. package/dist/utils/exit-codes.d.ts +47 -0
  186. package/dist/utils/exit-codes.d.ts.map +1 -0
  187. package/dist/utils/exit-codes.js +61 -0
  188. package/dist/utils/logger.d.ts +87 -0
  189. package/dist/utils/logger.d.ts.map +1 -0
  190. package/dist/utils/logger.js +185 -0
  191. package/dist/utils/sanitize.d.ts +36 -0
  192. package/dist/utils/sanitize.d.ts.map +1 -0
  193. package/dist/utils/sanitize.js +64 -0
  194. package/dist/utils/validators.d.ts +41 -0
  195. package/dist/utils/validators.d.ts.map +1 -0
  196. package/dist/utils/validators.js +123 -0
  197. package/package.json +63 -0
  198. package/schemas/vertaaux.config.schema.json +103 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Baseline file management for tracking technical debt.
3
+ *
4
+ * Enables teams to:
5
+ * - Create baselines from audit results
6
+ * - Add individual issues to baseline (ignore)
7
+ * - Load and save baseline files
8
+ *
9
+ * Baseline is stored in .vertaaux/baseline.json and committed to git.
10
+ */
11
+ import { readFile, writeFile, mkdir } from "fs/promises";
12
+ import { existsSync } from "fs";
13
+ import { dirname, resolve } from "path";
14
+ import { execSync } from "child_process";
15
+ import { generateFingerprint } from "./hash.js";
16
+ /**
17
+ * Current baseline file format version.
18
+ */
19
+ export const BASELINE_VERSION = 1;
20
+ /**
21
+ * Default baseline file path.
22
+ */
23
+ export const DEFAULT_BASELINE_PATH = ".vertaaux/baseline.json";
24
+ /**
25
+ * Get the current user from git config.
26
+ *
27
+ * @returns Git user name or undefined if not available
28
+ */
29
+ function getGitUser() {
30
+ try {
31
+ const name = execSync("git config user.name", {
32
+ encoding: "utf-8",
33
+ stdio: ["pipe", "pipe", "pipe"],
34
+ }).trim();
35
+ return name || undefined;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ /**
42
+ * Get the rule ID from an issue, checking multiple field names.
43
+ */
44
+ function getRuleId(issue) {
45
+ return issue.ruleId || issue.rule_id || issue.id || "unknown";
46
+ }
47
+ /**
48
+ * Load a baseline file from disk.
49
+ *
50
+ * @param path - Path to baseline file (default: .vertaaux/baseline.json)
51
+ * @returns Baseline file contents or null if file doesn't exist
52
+ */
53
+ export async function loadBaseline(path = DEFAULT_BASELINE_PATH) {
54
+ const resolvedPath = resolve(process.cwd(), path);
55
+ if (!existsSync(resolvedPath)) {
56
+ return null;
57
+ }
58
+ try {
59
+ const content = await readFile(resolvedPath, "utf-8");
60
+ const baseline = JSON.parse(content);
61
+ // Validate version
62
+ if (baseline.version !== BASELINE_VERSION) {
63
+ console.warn(`Warning: Baseline version ${baseline.version} may not be fully compatible`);
64
+ }
65
+ return baseline;
66
+ }
67
+ catch (error) {
68
+ throw new Error(`Failed to load baseline: ${error instanceof Error ? error.message : String(error)}`);
69
+ }
70
+ }
71
+ /**
72
+ * Save a baseline file to disk.
73
+ *
74
+ * @param baseline - Baseline file contents
75
+ * @param path - Path to baseline file (default: .vertaaux/baseline.json)
76
+ */
77
+ export async function saveBaseline(baseline, path = DEFAULT_BASELINE_PATH) {
78
+ const resolvedPath = resolve(process.cwd(), path);
79
+ const dir = dirname(resolvedPath);
80
+ // Create directory if needed
81
+ if (!existsSync(dir)) {
82
+ await mkdir(dir, { recursive: true });
83
+ }
84
+ // Write with pretty JSON
85
+ const content = JSON.stringify(baseline, null, 2);
86
+ await writeFile(resolvedPath, content + "\n", "utf-8");
87
+ }
88
+ /**
89
+ * Create a new baseline from audit issues.
90
+ *
91
+ * @param issues - Issues from audit results
92
+ * @param url - URL that was audited
93
+ * @returns New baseline file
94
+ */
95
+ export function createBaseline(issues, url) {
96
+ const now = new Date().toISOString();
97
+ const baselinedBy = getGitUser();
98
+ const baselineIssues = issues.map((issue) => ({
99
+ fingerprint: generateFingerprint(issue),
100
+ ruleId: getRuleId(issue),
101
+ severity: issue.severity || "warning",
102
+ category: issue.category || "general",
103
+ description: issue.description || issue.title || "",
104
+ selector: issue.selector,
105
+ baselinedAt: now,
106
+ baselinedBy,
107
+ }));
108
+ return {
109
+ version: BASELINE_VERSION,
110
+ created: now,
111
+ updated: now,
112
+ url,
113
+ issues: baselineIssues,
114
+ };
115
+ }
116
+ /**
117
+ * Add an issue to an existing baseline.
118
+ *
119
+ * @param baseline - Existing baseline file
120
+ * @param issue - Issue to add
121
+ * @param reason - Optional reason for baselining
122
+ * @returns Updated baseline file
123
+ */
124
+ export function addToBaseline(baseline, issue, reason) {
125
+ const now = new Date().toISOString();
126
+ const baselinedBy = getGitUser();
127
+ const fingerprint = generateFingerprint(issue);
128
+ // Check if already baselined
129
+ const existing = baseline.issues.find((i) => i.fingerprint === fingerprint);
130
+ if (existing) {
131
+ // Update reason if provided
132
+ if (reason) {
133
+ existing.reason = reason;
134
+ }
135
+ return {
136
+ ...baseline,
137
+ updated: now,
138
+ };
139
+ }
140
+ // Add new issue
141
+ const newIssue = {
142
+ fingerprint,
143
+ ruleId: getRuleId(issue),
144
+ severity: issue.severity || "warning",
145
+ category: issue.category || "general",
146
+ description: issue.description || issue.title || "",
147
+ selector: issue.selector,
148
+ reason,
149
+ baselinedAt: now,
150
+ baselinedBy,
151
+ };
152
+ return {
153
+ ...baseline,
154
+ updated: now,
155
+ issues: [...baseline.issues, newIssue],
156
+ };
157
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Caching module for VertaaUX CLI.
3
+ *
4
+ * Provides route-level caching to speed up repeated audits
5
+ * by skipping unchanged routes.
6
+ */
7
+ export { loadRouteCache, saveRouteCache, getCacheKey, generateContentHash, isCacheValid, getCachedIssues, updateCache, clearExpiredEntries, getCacheStats, type RouteCache, type RouteCacheEntry, } from "./route-cache.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,cAAc,EACd,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,WAAW,EACX,mBAAmB,EACnB,aAAa,EACb,KAAK,UAAU,EACf,KAAK,eAAe,GACrB,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Caching module for VertaaUX CLI.
3
+ *
4
+ * Provides route-level caching to speed up repeated audits
5
+ * by skipping unchanged routes.
6
+ */
7
+ export { loadRouteCache, saveRouteCache, getCacheKey, generateContentHash, isCacheValid, getCachedIssues, updateCache, clearExpiredEntries, getCacheStats, } from "./route-cache.js";
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Route-level caching for incremental audits.
3
+ *
4
+ * Caches audit results per route with content-based invalidation.
5
+ * This enables skipping audits for routes that haven't changed,
6
+ * significantly reducing CI time for repeated runs.
7
+ */
8
+ /**
9
+ * Cache entry for a single route.
10
+ */
11
+ export interface RouteCacheEntry {
12
+ /** Route URL that was audited */
13
+ route: string;
14
+ /** Content hash of the page (for invalidation) */
15
+ contentHash: string;
16
+ /** Fingerprints of issues found (for reconstruction) */
17
+ issueFingerprints: string[];
18
+ /** When this entry was cached */
19
+ cachedAt: string;
20
+ /** CLI version that produced this cache entry */
21
+ cliVersion: string;
22
+ /** Time taken to audit this route (for metrics) */
23
+ auditDuration?: number;
24
+ }
25
+ /**
26
+ * Full route cache structure.
27
+ */
28
+ export interface RouteCache {
29
+ /** Cache format version for migration handling */
30
+ version: number;
31
+ /** Cached entries keyed by route URL */
32
+ entries: Record<string, RouteCacheEntry>;
33
+ /** When cache was last written */
34
+ lastUpdated?: string;
35
+ }
36
+ /**
37
+ * Load route cache from disk.
38
+ *
39
+ * @param projectRoot - Project root directory (defaults to cwd)
40
+ * @returns RouteCache object (empty if file doesn't exist or is invalid)
41
+ */
42
+ export declare function loadRouteCache(projectRoot?: string): RouteCache;
43
+ /**
44
+ * Save route cache to disk.
45
+ *
46
+ * @param cache - RouteCache to save
47
+ * @param projectRoot - Project root directory (defaults to cwd)
48
+ */
49
+ export declare function saveRouteCache(cache: RouteCache, projectRoot?: string): void;
50
+ /**
51
+ * Get cache key for a route based on content hash.
52
+ *
53
+ * The cache key combines route URL and content hash to uniquely
54
+ * identify a specific version of a page.
55
+ *
56
+ * @param route - Route URL
57
+ * @param contentHash - Hash of page content
58
+ * @returns Cache key string
59
+ */
60
+ export declare function getCacheKey(route: string, contentHash: string): string;
61
+ /**
62
+ * Generate content hash from HTML content.
63
+ *
64
+ * Normalizes the content to reduce false cache invalidations
65
+ * from irrelevant changes (timestamps, random IDs, etc.).
66
+ *
67
+ * @param html - HTML content of the page
68
+ * @returns Content hash string
69
+ */
70
+ export declare function generateContentHash(html: string): string;
71
+ /**
72
+ * Check if a cached entry is valid for the given content.
73
+ *
74
+ * @param entry - Cached entry to validate
75
+ * @param contentHash - Current content hash
76
+ * @returns true if cache is valid
77
+ */
78
+ export declare function isCacheValid(entry: RouteCacheEntry, contentHash: string): boolean;
79
+ /**
80
+ * Get cached issue fingerprints for a route if cache is valid.
81
+ *
82
+ * @param cache - Route cache to check
83
+ * @param route - Route URL to look up
84
+ * @param contentHash - Current content hash for validation
85
+ * @returns Array of issue fingerprints if cached, null otherwise
86
+ */
87
+ export declare function getCachedIssues(cache: RouteCache, route: string, contentHash: string): string[] | null;
88
+ /**
89
+ * Update cache with new audit results for a route.
90
+ *
91
+ * @param cache - Route cache to update (mutated in place)
92
+ * @param route - Route URL that was audited
93
+ * @param contentHash - Content hash of the page
94
+ * @param issueFingerprints - Fingerprints of issues found
95
+ * @param auditDuration - Time taken to audit (optional)
96
+ */
97
+ export declare function updateCache(cache: RouteCache, route: string, contentHash: string, issueFingerprints: string[], auditDuration?: number): void;
98
+ /**
99
+ * Clear expired or invalid cache entries.
100
+ *
101
+ * @param cache - Route cache to clean (mutated in place)
102
+ * @param maxAge - Maximum age in milliseconds (default: 7 days)
103
+ * @returns Number of entries removed
104
+ */
105
+ export declare function clearExpiredEntries(cache: RouteCache, maxAge?: number): number;
106
+ /**
107
+ * Get cache statistics.
108
+ *
109
+ * @param cache - Route cache to analyze
110
+ * @returns Statistics object
111
+ */
112
+ export declare function getCacheStats(cache: RouteCache): {
113
+ totalEntries: number;
114
+ totalIssues: number;
115
+ oldestEntry: string | null;
116
+ newestEntry: string | null;
117
+ averageAuditDuration: number | null;
118
+ };
119
+ //# sourceMappingURL=route-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-cache.d.ts","sourceRoot":"","sources":["../../src/cache/route-cache.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAQD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CAsB/D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAiB5E;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAKtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAexD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,eAAe,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAiBT;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,EAAE,GAAG,IAAI,CAYjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM,EAAE,EAC3B,aAAa,CAAC,EAAE,MAAM,GACrB,IAAI,CASN;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,UAAU,EACjB,MAAM,SAA0B,GAC/B,MAAM,CAeR;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC,CA+BA"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Route-level caching for incremental audits.
3
+ *
4
+ * Caches audit results per route with content-based invalidation.
5
+ * This enables skipping audits for routes that haven't changed,
6
+ * significantly reducing CI time for repeated runs.
7
+ */
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { createHash } from "crypto";
11
+ // Get CLI version for cache invalidation
12
+ const CLI_VERSION = "0.1.0";
13
+ /** Default cache file location */
14
+ const CACHE_FILE = ".vertaaux/route-cache.json";
15
+ /** Current cache format version */
16
+ const CACHE_VERSION = 1;
17
+ /**
18
+ * Load route cache from disk.
19
+ *
20
+ * @param projectRoot - Project root directory (defaults to cwd)
21
+ * @returns RouteCache object (empty if file doesn't exist or is invalid)
22
+ */
23
+ export function loadRouteCache(projectRoot) {
24
+ const root = projectRoot || process.cwd();
25
+ const cachePath = path.join(root, CACHE_FILE);
26
+ try {
27
+ if (fs.existsSync(cachePath)) {
28
+ const content = fs.readFileSync(cachePath, "utf-8");
29
+ const cache = JSON.parse(content);
30
+ // Validate cache version
31
+ if (cache.version !== CACHE_VERSION) {
32
+ // Version mismatch, return empty cache
33
+ return { version: CACHE_VERSION, entries: {} };
34
+ }
35
+ return cache;
36
+ }
37
+ }
38
+ catch {
39
+ // Invalid cache file, return empty
40
+ }
41
+ return { version: CACHE_VERSION, entries: {} };
42
+ }
43
+ /**
44
+ * Save route cache to disk.
45
+ *
46
+ * @param cache - RouteCache to save
47
+ * @param projectRoot - Project root directory (defaults to cwd)
48
+ */
49
+ export function saveRouteCache(cache, projectRoot) {
50
+ const root = projectRoot || process.cwd();
51
+ const cachePath = path.join(root, CACHE_FILE);
52
+ // Ensure directory exists
53
+ const dir = path.dirname(cachePath);
54
+ if (!fs.existsSync(dir)) {
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ }
57
+ // Update timestamp
58
+ cache.lastUpdated = new Date().toISOString();
59
+ // Write atomically (write to temp, then rename)
60
+ const tempPath = `${cachePath}.tmp`;
61
+ fs.writeFileSync(tempPath, JSON.stringify(cache, null, 2), "utf-8");
62
+ fs.renameSync(tempPath, cachePath);
63
+ }
64
+ /**
65
+ * Get cache key for a route based on content hash.
66
+ *
67
+ * The cache key combines route URL and content hash to uniquely
68
+ * identify a specific version of a page.
69
+ *
70
+ * @param route - Route URL
71
+ * @param contentHash - Hash of page content
72
+ * @returns Cache key string
73
+ */
74
+ export function getCacheKey(route, contentHash) {
75
+ return createHash("sha256")
76
+ .update(`${route}:${contentHash}`)
77
+ .digest("hex")
78
+ .slice(0, 16);
79
+ }
80
+ /**
81
+ * Generate content hash from HTML content.
82
+ *
83
+ * Normalizes the content to reduce false cache invalidations
84
+ * from irrelevant changes (timestamps, random IDs, etc.).
85
+ *
86
+ * @param html - HTML content of the page
87
+ * @returns Content hash string
88
+ */
89
+ export function generateContentHash(html) {
90
+ // Normalize content for hashing
91
+ const normalized = html
92
+ // Remove inline timestamps/dates
93
+ .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/g, "TIMESTAMP")
94
+ // Remove random IDs (common patterns)
95
+ .replace(/id="[a-f0-9-]{36}"/g, 'id="UUID"')
96
+ .replace(/data-id="[^"]+"/g, 'data-id="ID"')
97
+ // Remove nonce attributes (CSP)
98
+ .replace(/nonce="[^"]+"/g, 'nonce="NONCE"')
99
+ // Normalize whitespace
100
+ .replace(/\s+/g, " ")
101
+ .trim();
102
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 32);
103
+ }
104
+ /**
105
+ * Check if a cached entry is valid for the given content.
106
+ *
107
+ * @param entry - Cached entry to validate
108
+ * @param contentHash - Current content hash
109
+ * @returns true if cache is valid
110
+ */
111
+ export function isCacheValid(entry, contentHash) {
112
+ // Content must match
113
+ if (entry.contentHash !== contentHash) {
114
+ return false;
115
+ }
116
+ // CLI version must match (avoid using old results with new rules)
117
+ if (entry.cliVersion !== CLI_VERSION) {
118
+ return false;
119
+ }
120
+ // Could add TTL check here if desired:
121
+ // const cachedTime = new Date(entry.cachedAt).getTime();
122
+ // const maxAge = 24 * 60 * 60 * 1000; // 24 hours
123
+ // if (Date.now() - cachedTime > maxAge) return false;
124
+ return true;
125
+ }
126
+ /**
127
+ * Get cached issue fingerprints for a route if cache is valid.
128
+ *
129
+ * @param cache - Route cache to check
130
+ * @param route - Route URL to look up
131
+ * @param contentHash - Current content hash for validation
132
+ * @returns Array of issue fingerprints if cached, null otherwise
133
+ */
134
+ export function getCachedIssues(cache, route, contentHash) {
135
+ const entry = cache.entries[route];
136
+ if (!entry) {
137
+ return null;
138
+ }
139
+ if (!isCacheValid(entry, contentHash)) {
140
+ return null;
141
+ }
142
+ return entry.issueFingerprints;
143
+ }
144
+ /**
145
+ * Update cache with new audit results for a route.
146
+ *
147
+ * @param cache - Route cache to update (mutated in place)
148
+ * @param route - Route URL that was audited
149
+ * @param contentHash - Content hash of the page
150
+ * @param issueFingerprints - Fingerprints of issues found
151
+ * @param auditDuration - Time taken to audit (optional)
152
+ */
153
+ export function updateCache(cache, route, contentHash, issueFingerprints, auditDuration) {
154
+ cache.entries[route] = {
155
+ route,
156
+ contentHash,
157
+ issueFingerprints,
158
+ cachedAt: new Date().toISOString(),
159
+ cliVersion: CLI_VERSION,
160
+ auditDuration,
161
+ };
162
+ }
163
+ /**
164
+ * Clear expired or invalid cache entries.
165
+ *
166
+ * @param cache - Route cache to clean (mutated in place)
167
+ * @param maxAge - Maximum age in milliseconds (default: 7 days)
168
+ * @returns Number of entries removed
169
+ */
170
+ export function clearExpiredEntries(cache, maxAge = 7 * 24 * 60 * 60 * 1000) {
171
+ const now = Date.now();
172
+ let removed = 0;
173
+ for (const [route, entry] of Object.entries(cache.entries)) {
174
+ const entryAge = now - new Date(entry.cachedAt).getTime();
175
+ // Remove if too old or wrong CLI version
176
+ if (entryAge > maxAge || entry.cliVersion !== CLI_VERSION) {
177
+ delete cache.entries[route];
178
+ removed++;
179
+ }
180
+ }
181
+ return removed;
182
+ }
183
+ /**
184
+ * Get cache statistics.
185
+ *
186
+ * @param cache - Route cache to analyze
187
+ * @returns Statistics object
188
+ */
189
+ export function getCacheStats(cache) {
190
+ const entries = Object.values(cache.entries);
191
+ if (entries.length === 0) {
192
+ return {
193
+ totalEntries: 0,
194
+ totalIssues: 0,
195
+ oldestEntry: null,
196
+ newestEntry: null,
197
+ averageAuditDuration: null,
198
+ };
199
+ }
200
+ const sorted = entries.sort((a, b) => new Date(a.cachedAt).getTime() - new Date(b.cachedAt).getTime());
201
+ const durations = entries
202
+ .map((e) => e.auditDuration)
203
+ .filter((d) => d !== undefined);
204
+ return {
205
+ totalEntries: entries.length,
206
+ totalIssues: entries.reduce((sum, e) => sum + e.issueFingerprints.length, 0),
207
+ oldestEntry: sorted[0].cachedAt,
208
+ newestEntry: sorted[sorted.length - 1].cachedAt,
209
+ averageAuditDuration: durations.length > 0
210
+ ? durations.reduce((a, b) => a + b, 0) / durations.length
211
+ : null,
212
+ };
213
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Detect routes changed in PR for incremental auditing.
3
+ *
4
+ * Uses git diff to find changed files, then maps them to routes.
5
+ * This enables efficient CI by only auditing what changed.
6
+ *
7
+ * Implements CICD-07: Incremental mode audits only routes touched in PR.
8
+ */
9
+ /**
10
+ * Options for detecting changed routes.
11
+ */
12
+ export interface ChangedRoutesOptions {
13
+ /** Base branch to compare against */
14
+ baseBranch: string;
15
+ /** Route patterns to watch (glob patterns for file paths) */
16
+ routePatterns: string[];
17
+ /** Mapping from file glob patterns to route URLs */
18
+ fileToRouteMap?: Record<string, string[]>;
19
+ }
20
+ /**
21
+ * Result of changed routes detection.
22
+ */
23
+ export interface ChangedRoutesResult {
24
+ /** Routes that were changed */
25
+ routes: string[];
26
+ /** Files that were changed (for debugging) */
27
+ changedFiles: string[];
28
+ /** Whether any relevant changes were found */
29
+ hasChanges: boolean;
30
+ }
31
+ /**
32
+ * Convert a file path to a route URL.
33
+ *
34
+ * Supports common framework conventions:
35
+ * - Next.js App Router: src/app/about/page.tsx -> /about
36
+ * - Next.js Pages Router: src/pages/blog/index.tsx -> /blog
37
+ * - SvelteKit: src/routes/blog/+page.svelte -> /blog
38
+ *
39
+ * @param filePath - The changed file path
40
+ * @returns The route URL or null if not a route file
41
+ */
42
+ export declare function filePathToRoute(filePath: string): string | null;
43
+ /**
44
+ * Detect base branch from CI environment variables.
45
+ *
46
+ * Supports:
47
+ * - GitHub Actions: GITHUB_BASE_REF
48
+ * - GitLab CI: CI_MERGE_REQUEST_TARGET_BRANCH_NAME
49
+ * - Azure DevOps: SYSTEM_PULLREQUEST_TARGETBRANCH
50
+ * - Jenkins: CHANGE_TARGET
51
+ * - CircleCI: CIRCLE_BRANCH (for base, uses main as fallback)
52
+ *
53
+ * @returns Detected base branch or "main" as default
54
+ */
55
+ export declare function detectBaseBranch(): string;
56
+ /**
57
+ * Get list of routes affected by changed files.
58
+ *
59
+ * Uses git diff to find changed files, then maps to routes.
60
+ *
61
+ * @param options - Detection options
62
+ * @returns Changed routes result
63
+ */
64
+ export declare function getChangedRoutes(options: ChangedRoutesOptions): Promise<ChangedRoutesResult>;
65
+ /**
66
+ * Budget modes for controlling audit scope.
67
+ *
68
+ * CICD-13: Default mode runs under sane budget; full scan is opt-in.
69
+ */
70
+ export declare const BUDGET_MODES: {
71
+ /** Quick scan: 5 pages, 30s timeout, 2 concurrent */
72
+ readonly quick: {
73
+ readonly maxPages: 5;
74
+ readonly maxTime: 30000;
75
+ readonly concurrency: 2;
76
+ };
77
+ /** Standard scan: 20 pages, 2min timeout, 4 concurrent */
78
+ readonly standard: {
79
+ readonly maxPages: 20;
80
+ readonly maxTime: 120000;
81
+ readonly concurrency: 4;
82
+ };
83
+ /** Full scan: unlimited pages, 10min timeout, 8 concurrent */
84
+ readonly full: {
85
+ readonly maxPages: number;
86
+ readonly maxTime: 600000;
87
+ readonly concurrency: 8;
88
+ };
89
+ };
90
+ export type BudgetMode = keyof typeof BUDGET_MODES;
91
+ /**
92
+ * Get budget configuration for a mode.
93
+ */
94
+ export declare function getBudgetConfig(mode: BudgetMode): (typeof BUDGET_MODES)[BudgetMode];
95
+ //# sourceMappingURL=changed-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changed-routes.d.ts","sourceRoot":"","sources":["../../src/ci/changed-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,+BAA+B;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,8CAA8C;IAC9C,UAAU,EAAE,OAAO,CAAC;CACrB;AAqCD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyE/D;AA8DD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAsDzC;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAoD9B;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY;IACvB,qDAAqD;;;;;;IAErD,0DAA0D;;;;;;IAE1D,8DAA8D;;;;;;CAEtD,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,YAAY,CAAC;AAEnD;;GAEG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,GACf,CAAC,OAAO,YAAY,CAAC,CAAC,UAAU,CAAC,CAEnC"}