@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.
- package/README.md +345 -0
- package/dist/auth/ci-token.d.ts +49 -0
- package/dist/auth/ci-token.d.ts.map +1 -0
- package/dist/auth/ci-token.js +83 -0
- package/dist/auth/device-flow.d.ts +66 -0
- package/dist/auth/device-flow.d.ts.map +1 -0
- package/dist/auth/device-flow.js +156 -0
- package/dist/auth/token-store.d.ts +53 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +78 -0
- package/dist/baseline/diff.d.ts +57 -0
- package/dist/baseline/diff.d.ts.map +1 -0
- package/dist/baseline/diff.js +152 -0
- package/dist/baseline/hash.d.ts +54 -0
- package/dist/baseline/hash.d.ts.map +1 -0
- package/dist/baseline/hash.js +66 -0
- package/dist/baseline/manager.d.ts +89 -0
- package/dist/baseline/manager.d.ts.map +1 -0
- package/dist/baseline/manager.js +157 -0
- package/dist/cache/index.d.ts +8 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +7 -0
- package/dist/cache/route-cache.d.ts +119 -0
- package/dist/cache/route-cache.d.ts.map +1 -0
- package/dist/cache/route-cache.js +213 -0
- package/dist/ci/changed-routes.d.ts +95 -0
- package/dist/ci/changed-routes.d.ts.map +1 -0
- package/dist/ci/changed-routes.js +304 -0
- package/dist/ci/github-api.d.ts +68 -0
- package/dist/ci/github-api.d.ts.map +1 -0
- package/dist/ci/github-api.js +138 -0
- package/dist/ci/gitlab-api.d.ts +75 -0
- package/dist/ci/gitlab-api.d.ts.map +1 -0
- package/dist/ci/gitlab-api.js +180 -0
- package/dist/ci/index.d.ts +6 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +4 -0
- package/dist/commands/audit.d.ts +58 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +862 -0
- package/dist/commands/baseline.d.ts +22 -0
- package/dist/commands/baseline.d.ts.map +1 -0
- package/dist/commands/baseline.js +210 -0
- package/dist/commands/comment.d.ts +14 -0
- package/dist/commands/comment.d.ts.map +1 -0
- package/dist/commands/comment.js +363 -0
- package/dist/commands/diff.d.ts +24 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +196 -0
- package/dist/commands/doctor.d.ts +58 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +338 -0
- package/dist/commands/download.d.ts +12 -0
- package/dist/commands/download.d.ts.map +1 -0
- package/dist/commands/download.js +183 -0
- package/dist/commands/explain.d.ts +62 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +302 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +212 -0
- package/dist/commands/login.d.ts +14 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +222 -0
- package/dist/commands/policy.d.ts +13 -0
- package/dist/commands/policy.d.ts.map +1 -0
- package/dist/commands/policy.js +347 -0
- package/dist/commands/upload.d.ts +12 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +158 -0
- package/dist/config/defaults.d.ts +21 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +49 -0
- package/dist/config/loader.d.ts +66 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +167 -0
- package/dist/config/schema.d.ts +55 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +6 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1090 -0
- package/dist/interactive/fix-wizard.d.ts +44 -0
- package/dist/interactive/fix-wizard.d.ts.map +1 -0
- package/dist/interactive/fix-wizard.js +286 -0
- package/dist/interactive/init-wizard.d.ts +32 -0
- package/dist/interactive/init-wizard.d.ts.map +1 -0
- package/dist/interactive/init-wizard.js +193 -0
- package/dist/interactive/prompts.d.ts +62 -0
- package/dist/interactive/prompts.d.ts.map +1 -0
- package/dist/interactive/prompts.js +78 -0
- package/dist/monorepo/detector.d.ts +70 -0
- package/dist/monorepo/detector.d.ts.map +1 -0
- package/dist/monorepo/detector.js +278 -0
- package/dist/monorepo/index.d.ts +9 -0
- package/dist/monorepo/index.d.ts.map +1 -0
- package/dist/monorepo/index.js +8 -0
- package/dist/monorepo/workspace.d.ts +142 -0
- package/dist/monorepo/workspace.d.ts.map +1 -0
- package/dist/monorepo/workspace.js +171 -0
- package/dist/output/envelope.d.ts +21 -0
- package/dist/output/envelope.d.ts.map +1 -0
- package/dist/output/envelope.js +27 -0
- package/dist/output/factory.d.ts +73 -0
- package/dist/output/factory.d.ts.map +1 -0
- package/dist/output/factory.js +60 -0
- package/dist/output/formats.d.ts +11 -0
- package/dist/output/formats.d.ts.map +1 -0
- package/dist/output/formats.js +41 -0
- package/dist/output/html.d.ts +45 -0
- package/dist/output/html.d.ts.map +1 -0
- package/dist/output/html.js +607 -0
- package/dist/output/human.d.ts +41 -0
- package/dist/output/human.d.ts.map +1 -0
- package/dist/output/human.js +274 -0
- package/dist/output/json.d.ts +42 -0
- package/dist/output/json.d.ts.map +1 -0
- package/dist/output/json.js +37 -0
- package/dist/output/junit.d.ts +56 -0
- package/dist/output/junit.d.ts.map +1 -0
- package/dist/output/junit.js +135 -0
- package/dist/output/markdown.d.ts +77 -0
- package/dist/output/markdown.d.ts.map +1 -0
- package/dist/output/markdown.js +411 -0
- package/dist/output/sarif.d.ts +160 -0
- package/dist/output/sarif.d.ts.map +1 -0
- package/dist/output/sarif.js +207 -0
- package/dist/policy/evaluator.d.ts +111 -0
- package/dist/policy/evaluator.d.ts.map +1 -0
- package/dist/policy/evaluator.js +362 -0
- package/dist/policy/index.d.ts +15 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +11 -0
- package/dist/policy/loader.d.ts +97 -0
- package/dist/policy/loader.d.ts.map +1 -0
- package/dist/policy/loader.js +281 -0
- package/dist/policy/schema.d.ts +297 -0
- package/dist/policy/schema.d.ts.map +1 -0
- package/dist/policy/schema.js +230 -0
- package/dist/quality-gate/evaluator.d.ts +58 -0
- package/dist/quality-gate/evaluator.d.ts.map +1 -0
- package/dist/quality-gate/evaluator.js +274 -0
- package/dist/quality-gate/index.d.ts +10 -0
- package/dist/quality-gate/index.d.ts.map +1 -0
- package/dist/quality-gate/index.js +7 -0
- package/dist/quality-gate/types.d.ts +103 -0
- package/dist/quality-gate/types.d.ts.map +1 -0
- package/dist/quality-gate/types.js +23 -0
- package/dist/templates/azure-devops.d.ts +25 -0
- package/dist/templates/azure-devops.d.ts.map +1 -0
- package/dist/templates/azure-devops.js +109 -0
- package/dist/templates/circleci.d.ts +28 -0
- package/dist/templates/circleci.d.ts.map +1 -0
- package/dist/templates/circleci.js +86 -0
- package/dist/templates/github-actions.d.ts +81 -0
- package/dist/templates/github-actions.d.ts.map +1 -0
- package/dist/templates/github-actions.js +393 -0
- package/dist/templates/gitlab-ci.d.ts +26 -0
- package/dist/templates/gitlab-ci.d.ts.map +1 -0
- package/dist/templates/gitlab-ci.js +70 -0
- package/dist/templates/index.d.ts +72 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +112 -0
- package/dist/templates/jenkins.d.ts +26 -0
- package/dist/templates/jenkins.d.ts.map +1 -0
- package/dist/templates/jenkins.js +110 -0
- package/dist/ui/banner.d.ts +31 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +84 -0
- package/dist/ui/diagnostics.d.ts +39 -0
- package/dist/ui/diagnostics.d.ts.map +1 -0
- package/dist/ui/diagnostics.js +153 -0
- package/dist/ui/spinner.d.ts +61 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +101 -0
- package/dist/ui/table.d.ts +63 -0
- package/dist/ui/table.d.ts.map +1 -0
- package/dist/ui/table.js +236 -0
- package/dist/utils/client.d.ts +82 -0
- package/dist/utils/client.d.ts.map +1 -0
- package/dist/utils/client.js +128 -0
- package/dist/utils/detect-env.d.ts +59 -0
- package/dist/utils/detect-env.d.ts.map +1 -0
- package/dist/utils/detect-env.js +115 -0
- package/dist/utils/exit-codes.d.ts +47 -0
- package/dist/utils/exit-codes.d.ts.map +1 -0
- package/dist/utils/exit-codes.js +61 -0
- package/dist/utils/logger.d.ts +87 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +185 -0
- package/dist/utils/sanitize.d.ts +36 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +64 -0
- package/dist/utils/validators.d.ts +41 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +123 -0
- package/package.json +63 -0
- 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"}
|