effect-v4-audit 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/demo/effect-v4-audit-demo.gif +0 -0
- package/package.json +12 -6
- package/src/bin.ts +100 -138
- package/src/diagnostics.ts +124 -0
- package/src/index.ts +2 -1
package/README.md
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
# effect-v4-audit
|
|
2
2
|
|
|
3
3
|
Deterministic CLI audit for v3-era Effect APIs in Effect v4 migrations.
|
|
4
|
+
Built on Effect v4 beta CLI APIs (`effect/unstable/cli`).
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## Demo
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bunx effect-v4-audit --help
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Local Development
|
|
6
17
|
|
|
7
18
|
```bash
|
|
8
|
-
|
|
19
|
+
git clone https://github.com/agustif/effect-v4-audit.git
|
|
20
|
+
cd effect-v4-audit
|
|
9
21
|
bun install
|
|
10
22
|
```
|
|
11
23
|
|
|
12
|
-
|
|
24
|
+
Then run:
|
|
13
25
|
|
|
14
26
|
```bash
|
|
15
|
-
bun
|
|
27
|
+
bun run ./src/bin.ts --help
|
|
16
28
|
```
|
|
17
29
|
|
|
18
30
|
## Usage
|
|
@@ -22,13 +34,15 @@ effect-v4-audit \
|
|
|
22
34
|
--cwd . \
|
|
23
35
|
--pattern "**/*.{ts,tsx,mts,cts}" \
|
|
24
36
|
--ignore "**/*.gen.ts" \
|
|
25
|
-
--
|
|
37
|
+
--color auto \
|
|
38
|
+
--format diagnostic \
|
|
26
39
|
--fail-on-findings
|
|
27
40
|
```
|
|
28
41
|
|
|
29
42
|
## Output
|
|
30
43
|
|
|
31
|
-
- `
|
|
44
|
+
- `diagnostic`: rust-style diagnostics with source snippet and `help` hint.
|
|
45
|
+
- `table`: compact findings with path, line/column, and migration suggestion.
|
|
32
46
|
- `json`: stable machine-readable report for CI.
|
|
33
47
|
|
|
34
48
|
## Guarantees
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-v4-audit",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"bun": ">=1.3.0"
|
|
7
7
|
},
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"description": "Deterministic Effect v4 migration audit CLI",
|
|
10
|
-
"homepage": "https://effect
|
|
10
|
+
"homepage": "https://github.com/agustif/effect-v4-audit",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/agustif/effect-v4-audit/issues"
|
|
13
|
+
},
|
|
11
14
|
"repository": {
|
|
12
15
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/
|
|
14
|
-
"directory": "packages/tools/effect-v4-audit"
|
|
16
|
+
"url": "git+https://github.com/agustif/effect-v4-audit.git"
|
|
15
17
|
},
|
|
16
18
|
"sideEffects": [],
|
|
17
19
|
"bin": {
|
|
18
|
-
"effect-v4-audit": "
|
|
20
|
+
"effect-v4-audit": "bin.js"
|
|
19
21
|
},
|
|
20
22
|
"exports": {
|
|
21
23
|
".": "./src/index.ts",
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
},
|
|
25
27
|
"files": [
|
|
26
28
|
"bin.js",
|
|
29
|
+
"demo/effect-v4-audit-demo.gif",
|
|
27
30
|
"src/**/*.ts",
|
|
28
31
|
"README.md"
|
|
29
32
|
],
|
|
@@ -34,9 +37,12 @@
|
|
|
34
37
|
"coverage": "bun test --coverage"
|
|
35
38
|
},
|
|
36
39
|
"publishConfig": {
|
|
37
|
-
"access": "public"
|
|
40
|
+
"access": "public",
|
|
41
|
+
"provenance": true
|
|
38
42
|
},
|
|
39
43
|
"dependencies": {
|
|
44
|
+
"@effect/platform-node": "4.0.0-beta.12",
|
|
45
|
+
"effect": "4.0.0-beta.12",
|
|
40
46
|
"glob": "^13.0.0"
|
|
41
47
|
}
|
|
42
48
|
}
|
package/src/bin.ts
CHANGED
|
@@ -1,157 +1,119 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import * as NodeServices from "@effect/platform-node/NodeServices"
|
|
3
4
|
import { readFile } from "node:fs/promises"
|
|
4
5
|
import path from "node:path"
|
|
5
6
|
import { glob } from "glob"
|
|
7
|
+
import * as Console from "effect/Console"
|
|
8
|
+
import * as Effect from "effect/Effect"
|
|
9
|
+
import * as CliError from "effect/unstable/cli/CliError"
|
|
10
|
+
import * as Command from "effect/unstable/cli/Command"
|
|
11
|
+
import * as Flag from "effect/unstable/cli/Flag"
|
|
6
12
|
import { analyzeV4Sources } from "./analyze.ts"
|
|
13
|
+
import { renderV4AuditDiagnostics, type V4AuditColorMode } from "./diagnostics.ts"
|
|
7
14
|
import { renderV4AuditJson, renderV4AuditTable } from "./render.ts"
|
|
8
15
|
|
|
9
|
-
interface CliOptions {
|
|
10
|
-
readonly cwd: string
|
|
11
|
-
readonly pattern: string
|
|
12
|
-
readonly ignore: ReadonlyArray<string>
|
|
13
|
-
readonly format: "table" | "json"
|
|
14
|
-
readonly failOnFindings: boolean
|
|
15
|
-
}
|
|
16
|
-
|
|
17
16
|
const defaultIgnorePatterns = [
|
|
18
17
|
"**/node_modules/**",
|
|
19
18
|
"**/dist/**",
|
|
20
19
|
"**/coverage/**",
|
|
21
20
|
"**/.git/**",
|
|
22
21
|
"**/.turbo/**"
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
"
|
|
27
|
-
"",
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
index++
|
|
66
|
-
break
|
|
67
|
-
case "-p":
|
|
68
|
-
case "--pattern":
|
|
69
|
-
pattern = parseNextValue(args, index, arg)
|
|
70
|
-
index++
|
|
71
|
-
break
|
|
72
|
-
case "-i":
|
|
73
|
-
case "--ignore":
|
|
74
|
-
ignore.push(parseNextValue(args, index, arg))
|
|
75
|
-
index++
|
|
76
|
-
break
|
|
77
|
-
case "-f":
|
|
78
|
-
case "--format": {
|
|
79
|
-
const value = parseNextValue(args, index, arg)
|
|
80
|
-
if (value !== "table" && value !== "json") {
|
|
81
|
-
throw new Error(`Invalid format: ${value}`)
|
|
82
|
-
}
|
|
83
|
-
format = value
|
|
84
|
-
index++
|
|
85
|
-
break
|
|
86
|
-
}
|
|
87
|
-
case "--fail-on-findings":
|
|
88
|
-
failOnFindings = true
|
|
89
|
-
break
|
|
90
|
-
default:
|
|
91
|
-
throw new Error(`Unknown argument: ${arg}`)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
cwd,
|
|
97
|
-
pattern,
|
|
98
|
-
ignore,
|
|
99
|
-
format,
|
|
100
|
-
failOnFindings
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const toPosix = (file: string): string => file.split(path.sep).join("/")
|
|
105
|
-
|
|
106
|
-
const readSources = async (options: CliOptions): Promise<ReadonlyArray<{ readonly path: string; readonly content: string }>> => {
|
|
107
|
-
const root = path.resolve(options.cwd)
|
|
108
|
-
const files = await glob(options.pattern, {
|
|
109
|
-
cwd: root,
|
|
110
|
-
dot: false,
|
|
111
|
-
follow: false,
|
|
112
|
-
nodir: true,
|
|
113
|
-
ignore: [...defaultIgnorePatterns, ...options.ignore]
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const cwd = Flag.directory("cwd", { mustExist: true }).pipe(
|
|
25
|
+
Flag.withAlias("c"),
|
|
26
|
+
Flag.withDescription("Directory to scan"),
|
|
27
|
+
Flag.withDefault(".")
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const pattern = Flag.string("pattern").pipe(
|
|
31
|
+
Flag.withAlias("p"),
|
|
32
|
+
Flag.withDescription("Glob for files to scan"),
|
|
33
|
+
Flag.withDefault("**/*.{ts,tsx,mts,cts}")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const ignore = Flag.string("ignore").pipe(
|
|
37
|
+
Flag.withAlias("i"),
|
|
38
|
+
Flag.withDescription("Glob pattern(s) to ignore (repeat --ignore for multiple values)"),
|
|
39
|
+
Flag.between(0, Infinity)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const format = Flag.choice("format", ["diagnostic", "table", "json"] as const).pipe(
|
|
43
|
+
Flag.withAlias("f"),
|
|
44
|
+
Flag.withDescription("Report output format"),
|
|
45
|
+
Flag.withDefault("diagnostic")
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const color = Flag.choice("color", ["auto", "always", "never"] as const).pipe(
|
|
49
|
+
Flag.withDescription("ANSI color output mode"),
|
|
50
|
+
Flag.withDefault("auto")
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const failOnFindings = Flag.boolean("fail-on-findings").pipe(
|
|
54
|
+
Flag.withDescription("Exit with error code when at least one finding exists")
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const toUserError = (cause: unknown, fallback: string): CliError.UserError =>
|
|
58
|
+
new CliError.UserError({
|
|
59
|
+
cause: cause instanceof Error
|
|
60
|
+
? cause.message
|
|
61
|
+
: typeof cause === "string"
|
|
62
|
+
? cause
|
|
63
|
+
: fallback
|
|
114
64
|
})
|
|
115
65
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
66
|
+
const cli = Command.make("effect-v4-audit", {
|
|
67
|
+
cwd,
|
|
68
|
+
pattern,
|
|
69
|
+
ignore,
|
|
70
|
+
format,
|
|
71
|
+
color,
|
|
72
|
+
failOnFindings
|
|
73
|
+
}, (options) =>
|
|
74
|
+
Effect.gen(function*() {
|
|
75
|
+
const root = path.resolve(options.cwd)
|
|
76
|
+
const toPosix = path.sep === "/" ? (file: string) => file : (file: string) => file.split(path.sep).join("/")
|
|
77
|
+
|
|
78
|
+
const files = yield* Effect.tryPromise({
|
|
79
|
+
try: () =>
|
|
80
|
+
glob(options.pattern, {
|
|
81
|
+
cwd: root,
|
|
82
|
+
dot: false,
|
|
83
|
+
follow: false,
|
|
84
|
+
nodir: true,
|
|
85
|
+
ignore: [...defaultIgnorePatterns, ...options.ignore]
|
|
86
|
+
}),
|
|
87
|
+
catch: (cause) => toUserError(cause, "Failed to collect files")
|
|
88
|
+
}).pipe(Effect.map((files) => files.sort((a, b) => toPosix(a).localeCompare(toPosix(b)))))
|
|
89
|
+
|
|
90
|
+
const sources = yield* Effect.forEach(files, (file) =>
|
|
91
|
+
Effect.tryPromise({
|
|
92
|
+
try: () => readFile(path.join(root, file), "utf8"),
|
|
93
|
+
catch: (cause) => toUserError(cause, `Failed to read ${file}`)
|
|
94
|
+
}).pipe(Effect.map((content) => ({
|
|
123
95
|
path: toPosix(file),
|
|
124
96
|
content
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return 0
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (import.meta.main) {
|
|
150
|
-
const exitCode = await runCli(process.argv.slice(2)).catch((error: unknown) => {
|
|
151
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
152
|
-
console.error(message)
|
|
153
|
-
return 1
|
|
154
|
-
})
|
|
97
|
+
}))))
|
|
98
|
+
|
|
99
|
+
const report = analyzeV4Sources({ sources })
|
|
100
|
+
const output = options.format === "json"
|
|
101
|
+
? renderV4AuditJson(report)
|
|
102
|
+
: options.format === "table"
|
|
103
|
+
? renderV4AuditTable(report)
|
|
104
|
+
: renderV4AuditDiagnostics(report, sources, { color: options.color as V4AuditColorMode })
|
|
105
|
+
|
|
106
|
+
yield* Console.log(output)
|
|
107
|
+
|
|
108
|
+
if (options.failOnFindings && report.summary.findingCount > 0) {
|
|
109
|
+
yield* Effect.sync(() => {
|
|
110
|
+
process.exitCode = 1
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
const main = Command.run(cli, { version: "0.2.1" }).pipe(
|
|
116
|
+
Effect.provide(NodeServices.layer)
|
|
117
|
+
)
|
|
155
118
|
|
|
156
|
-
|
|
157
|
-
}
|
|
119
|
+
Effect.runPromise(main)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { V4AuditReport, V4AuditSourceFile } from "./types.ts"
|
|
2
|
+
|
|
3
|
+
export type V4AuditColorMode = "auto" | "always" | "never"
|
|
4
|
+
|
|
5
|
+
const resolveUseColor = (mode: V4AuditColorMode): boolean => {
|
|
6
|
+
if (mode === "always") {
|
|
7
|
+
return true
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (mode === "never") {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const globalProcess = (globalThis as any).process
|
|
15
|
+
const hasProcess = typeof globalProcess === "object" && globalProcess !== null
|
|
16
|
+
if (!hasProcess) {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (globalProcess.env?.NO_COLOR === "1") {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (globalProcess.env?.FORCE_COLOR && globalProcess.env.FORCE_COLOR !== "0") {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return globalProcess.stdout?.isTTY === true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const makeColor = (useColor: boolean) => {
|
|
32
|
+
if (!useColor) {
|
|
33
|
+
return {
|
|
34
|
+
bold: (text: string): string => text,
|
|
35
|
+
dim: (text: string): string => text,
|
|
36
|
+
red: (text: string): string => text,
|
|
37
|
+
yellow: (text: string): string => text,
|
|
38
|
+
blue: (text: string): string => text,
|
|
39
|
+
cyan: (text: string): string => text,
|
|
40
|
+
green: (text: string): string => text
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
bold: (text: string): string => `\x1b[1m${text}\x1b[0m`,
|
|
46
|
+
dim: (text: string): string => `\x1b[2m${text}\x1b[0m`,
|
|
47
|
+
red: (text: string): string => `\x1b[31m${text}\x1b[0m`,
|
|
48
|
+
yellow: (text: string): string => `\x1b[33m${text}\x1b[0m`,
|
|
49
|
+
blue: (text: string): string => `\x1b[34m${text}\x1b[0m`,
|
|
50
|
+
cyan: (text: string): string => `\x1b[36m${text}\x1b[0m`,
|
|
51
|
+
green: (text: string): string => `\x1b[32m${text}\x1b[0m`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lineAt = (content: string, line: number): string => {
|
|
56
|
+
const lines = content.split("\n")
|
|
57
|
+
if (line <= 0 || line > lines.length) {
|
|
58
|
+
return ""
|
|
59
|
+
}
|
|
60
|
+
return lines[line - 1] ?? ""
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const markerWidth = (match: string): number => Math.max(1, match.trim().length)
|
|
64
|
+
|
|
65
|
+
const digits = (value: number): number => `${Math.max(1, value)}`.length
|
|
66
|
+
|
|
67
|
+
const padLeft = (value: string, width: number): string => `${" ".repeat(Math.max(0, width - value.length))}${value}`
|
|
68
|
+
|
|
69
|
+
export const renderV4AuditDiagnostics = (
|
|
70
|
+
report: V4AuditReport,
|
|
71
|
+
sources: ReadonlyArray<V4AuditSourceFile>,
|
|
72
|
+
options?: {
|
|
73
|
+
readonly color?: V4AuditColorMode | undefined
|
|
74
|
+
}
|
|
75
|
+
): string => {
|
|
76
|
+
const color = makeColor(resolveUseColor(options?.color ?? "never"))
|
|
77
|
+
|
|
78
|
+
if (report.summary.findingCount === 0) {
|
|
79
|
+
return `No v4-audit findings. Files scanned: ${report.summary.filesScanned}.`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sourceMap = new Map<string, string>()
|
|
83
|
+
for (const source of sources) {
|
|
84
|
+
sourceMap.set(source.path.split("\\").join("/"), source.content)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const lines: Array<string> = [
|
|
88
|
+
`Findings: ${report.summary.findingCount} (errors: ${report.summary.errorCount}, warnings: ${report.summary.warningCount})`,
|
|
89
|
+
`Files scanned: ${report.summary.filesScanned}, files with findings: ${report.summary.filesWithFindings}`,
|
|
90
|
+
""
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
for (const finding of report.findings) {
|
|
94
|
+
const severity = finding.severity === "error"
|
|
95
|
+
? color.bold(color.red("error"))
|
|
96
|
+
: color.bold(color.yellow("warning"))
|
|
97
|
+
const ruleId = color.bold(color.cyan(finding.ruleId))
|
|
98
|
+
const sourceContent = sourceMap.get(finding.path)
|
|
99
|
+
const codeLine = sourceContent ? lineAt(sourceContent, finding.line) : ""
|
|
100
|
+
const lineNo = `${finding.line}`
|
|
101
|
+
const gutterWidth = digits(finding.line)
|
|
102
|
+
const marker = finding.severity === "error"
|
|
103
|
+
? color.red("^".repeat(markerWidth(finding.match)))
|
|
104
|
+
: color.yellow("^".repeat(markerWidth(finding.match)))
|
|
105
|
+
const columnOffset = Math.max(0, finding.column - 1)
|
|
106
|
+
|
|
107
|
+
lines.push(`${severity}[${ruleId}]: ${finding.message}`)
|
|
108
|
+
lines.push(` ${color.blue("-->")} ${finding.path}:${finding.line}:${finding.column}`)
|
|
109
|
+
lines.push(`${color.dim(" ".repeat(gutterWidth))} ${color.blue("|")}`)
|
|
110
|
+
|
|
111
|
+
if (codeLine.length > 0) {
|
|
112
|
+
lines.push(`${color.dim(padLeft(lineNo, gutterWidth))} ${color.blue("|")} ${codeLine}`)
|
|
113
|
+
lines.push(`${color.dim(" ".repeat(gutterWidth))} ${color.blue("|")} ${" ".repeat(columnOffset)}${marker}`)
|
|
114
|
+
} else {
|
|
115
|
+
lines.push(`${color.dim(padLeft(lineNo, gutterWidth))} ${color.blue("|")} ${finding.match}`)
|
|
116
|
+
lines.push(`${color.dim(" ".repeat(gutterWidth))} ${color.blue("|")} ${marker}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
lines.push(`${color.dim(" ".repeat(gutterWidth))} ${color.green("=")} ${color.green("help")}: ${finding.suggestion}`)
|
|
120
|
+
lines.push("")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lines.join("\n")
|
|
124
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { analyzeV4Sources } from "./analyze.ts"
|
|
2
|
+
import { renderV4AuditDiagnostics } from "./diagnostics.ts"
|
|
2
3
|
import { defaultV4AuditRules } from "./rules.ts"
|
|
3
4
|
import { renderV4AuditJson, renderV4AuditTable } from "./render.ts"
|
|
4
5
|
|
|
5
6
|
export * from "./types.ts"
|
|
6
|
-
export { analyzeV4Sources, defaultV4AuditRules, renderV4AuditJson, renderV4AuditTable }
|
|
7
|
+
export { analyzeV4Sources, defaultV4AuditRules, renderV4AuditDiagnostics, renderV4AuditJson, renderV4AuditTable }
|