@aarock1234/slopscan 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.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/SKILL.md +149 -0
- package/dist/analyzer.d.ts +5 -0
- package/dist/analyzer.js +13 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/analyzers/ast.d.ts +12 -0
- package/dist/analyzers/ast.js +73 -0
- package/dist/analyzers/ast.js.map +1 -0
- package/dist/analyzers/judge-chunks.d.ts +13 -0
- package/dist/analyzers/judge-chunks.js +87 -0
- package/dist/analyzers/judge-chunks.js.map +1 -0
- package/dist/analyzers/judge-validate.d.ts +19 -0
- package/dist/analyzers/judge-validate.js +81 -0
- package/dist/analyzers/judge-validate.js.map +1 -0
- package/dist/analyzers/judge.d.ts +44 -0
- package/dist/analyzers/judge.js +177 -0
- package/dist/analyzers/judge.js.map +1 -0
- package/dist/change.d.ts +19 -0
- package/dist/change.js +87 -0
- package/dist/change.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +125 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +91 -0
- package/dist/config.js +61 -0
- package/dist/config.js.map +1 -0
- package/dist/confirm.d.ts +11 -0
- package/dist/confirm.js +100 -0
- package/dist/confirm.js.map +1 -0
- package/dist/finding.d.ts +13 -0
- package/dist/finding.js +18 -0
- package/dist/finding.js.map +1 -0
- package/dist/lang.d.ts +16 -0
- package/dist/lang.js +37 -0
- package/dist/lang.js.map +1 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +34 -0
- package/dist/model.js.map +1 -0
- package/dist/report.d.ts +124 -0
- package/dist/report.js +112 -0
- package/dist/report.js.map +1 -0
- package/dist/rule.d.ts +66 -0
- package/dist/rule.js +259 -0
- package/dist/rule.js.map +1 -0
- package/dist/scan.d.ts +14 -0
- package/dist/scan.js +33 -0
- package/dist/scan.js.map +1 -0
- package/dist/score.d.ts +147 -0
- package/dist/score.js +142 -0
- package/dist/score.js.map +1 -0
- package/dist/shared/concurrency.d.ts +5 -0
- package/dist/shared/concurrency.js +17 -0
- package/dist/shared/concurrency.js.map +1 -0
- package/dist/shared/env.d.ts +27 -0
- package/dist/shared/env.js +29 -0
- package/dist/shared/env.js.map +1 -0
- package/dist/shared/errors.d.ts +18 -0
- package/dist/shared/errors.js +26 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/log.d.ts +2 -0
- package/dist/shared/log.js +18 -0
- package/dist/shared/log.js.map +1 -0
- package/dist/shared/paths.d.ts +3 -0
- package/dist/shared/paths.js +6 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/prompts.d.ts +3 -0
- package/dist/shared/prompts.js +34 -0
- package/dist/shared/prompts.js.map +1 -0
- package/package.json +79 -0
- package/prompts/judge.md +37 -0
- package/rules/any/futureproof/abstraction/pass-through-wrapper.md +70 -0
- package/rules/any/futureproof/abstraction/single-caller-helper.md +38 -0
- package/rules/any/futureproof/abstraction/single-impl-interface.md +83 -0
- package/rules/any/futureproof/exports/dead-export.md +62 -0
- package/rules/any/futureproof/layering/framework-type-in-domain.md +83 -0
- package/rules/any/futureproof/layering/logic-in-handler.md +94 -0
- package/rules/any/futureproof/params/boolean-positional-param.md +128 -0
- package/rules/any/futureproof/params/positional-config-args.md +135 -0
- package/rules/any/futureproof/structure/wide-function.md +138 -0
- package/rules/any/futureproof/testing/test-asserts-implementation.md +70 -0
- package/rules/any/hacky/comments/comment-restates-code.md +50 -0
- package/rules/any/hacky/comments/shipped-todo-comment.md +55 -0
- package/rules/any/hacky/concurrency/sleep-based-sync.md +95 -0
- package/rules/any/hacky/config/hardcoded-url.md +88 -0
- package/rules/any/hacky/constants/magic-number.md +94 -0
- package/rules/any/hacky/debugging/debug-print.md +73 -0
- package/rules/any/hacky/duplication/copy-paste-block.md +117 -0
- package/rules/any/hacky/types/stringly-typed-enum.md +86 -0
- package/rules/any/idiom/comments/no-section-banners.md +47 -0
- package/rules/go/futureproof/constants/string-enums-when-serialized.md +47 -0
- package/rules/go/futureproof/constants/typed-constants-for-enums.md +45 -0
- package/rules/go/futureproof/interfaces/compile-time-impl-assertion.md +43 -0
- package/rules/go/futureproof/interfaces/define-interfaces-at-consumer.md +47 -0
- package/rules/go/futureproof/interfaces/keep-interfaces-small.md +53 -0
- package/rules/go/futureproof/naming/no-utils-helpers-common-package.md +34 -0
- package/rules/go/futureproof/structs/zero-value-usable.md +50 -0
- package/rules/go/futureproof/types/generics-over-any.md +55 -0
- package/rules/go/hacky/concurrency/no-goroutine-without-wait.md +44 -0
- package/rules/go/hacky/context/never-pass-nil-context.md +64 -0
- package/rules/go/hacky/context/no-context-in-struct.md +44 -0
- package/rules/go/hacky/errors/check-close-error-on-writable.md +53 -0
- package/rules/go/hacky/errors/custom-error-type-errors-as.md +54 -0
- package/rules/go/hacky/errors/ignored-error-blank.md +84 -0
- package/rules/go/hacky/errors/panic-only-unrecoverable.md +77 -0
- package/rules/go/hacky/errors/sentinel-errors-with-errors-is.md +63 -0
- package/rules/go/hacky/errors/wrap-errors-with-w.md +71 -0
- package/rules/go/hacky/state/package-level-mutable-var.md +95 -0
- package/rules/go/hacky/structure/os-exit-only-in-main.md +61 -0
- package/rules/go/hacky/type-safety/no-map-string-any.md +49 -0
- package/rules/go/hacky/types/json-into-struct-not-map.md +48 -0
- package/rules/go/hacky/types/no-any.md +71 -0
- package/rules/go/hacky/types/no-map-any-any.md +35 -0
- package/rules/go/idiom/comments/doc-comment-on-exported.md +81 -0
- package/rules/go/idiom/comments/doc-comment-starts-with-name.md +36 -0
- package/rules/go/idiom/concurrency/mutex-over-channels-for-state.md +47 -0
- package/rules/go/idiom/constructors/constructor-named-new.md +37 -0
- package/rules/go/idiom/context/context-is-first-param.md +47 -0
- package/rules/go/idiom/context/io-takes-context.md +50 -0
- package/rules/go/idiom/errors/capitalized-error-message.md +52 -0
- package/rules/go/idiom/errors/check-error-immediately.md +45 -0
- package/rules/go/idiom/errors/defer-close-explicit-discard.md +65 -0
- package/rules/go/idiom/errors/error-is-last-return-value.md +53 -0
- package/rules/go/idiom/errors/no-log-and-return.md +45 -0
- package/rules/go/idiom/formatting/blank-line-before-return.md +48 -0
- package/rules/go/idiom/formatting/multiline-struct-literals.md +56 -0
- package/rules/go/idiom/imports/import-order-groups.md +42 -0
- package/rules/go/idiom/imports/side-effect-imports-own-group.md +51 -0
- package/rules/go/idiom/interfaces/accept-interfaces-return-concrete.md +39 -0
- package/rules/go/idiom/iterators/prefer-iter-seq.md +47 -0
- package/rules/go/idiom/logging/lowercase-log-messages.md +80 -0
- package/rules/go/idiom/logging/use-slog.md +46 -0
- package/rules/go/idiom/naming/acronyms-consistent-case.md +97 -0
- package/rules/go/idiom/naming/interface-er-suffix.md +33 -0
- package/rules/go/idiom/naming/no-package-name-stutter.md +37 -0
- package/rules/go/idiom/naming/package-name-single-lowercase-word.md +40 -0
- package/rules/go/idiom/receivers/consistent-receiver-kind-per-type.md +40 -0
- package/rules/go/idiom/receivers/no-this-receiver.md +37 -0
- package/rules/go/idiom/receivers/pointer-vs-value-receiver-choice.md +49 -0
- package/rules/go/idiom/receivers/short-receiver-names.md +43 -0
- package/rules/go/idiom/structs/new-expr-for-pointer-fields.md +40 -0
- package/rules/go/idiom/structure/main-delegates-to-run.md +54 -0
- package/rules/ts/futureproof/classes/composition-over-abstract-base.md +73 -0
- package/rules/ts/futureproof/functions/explicit-return-type-on-exports.md +50 -0
- package/rules/ts/futureproof/structure/colocate-zod-schemas.md +46 -0
- package/rules/ts/futureproof/types/exhaustive-switch-never-check.md +52 -0
- package/rules/ts/futureproof/types/readonly-for-immutable-data.md +46 -0
- package/rules/ts/futureproof/zod/enum-values-from-const-object.md +37 -0
- package/rules/ts/futureproof/zod/no-z-native-enum.md +36 -0
- package/rules/ts/futureproof/zod/schema-first-infer-type.md +46 -0
- package/rules/ts/hacky/async/no-floating-promises.md +52 -0
- package/rules/ts/hacky/config/env-read-outside-config.md +56 -0
- package/rules/ts/hacky/errors/catch-param-typed-unknown.md +67 -0
- package/rules/ts/hacky/errors/custom-error-class-instanceof.md +66 -0
- package/rules/ts/hacky/errors/empty-catch.md +52 -0
- package/rules/ts/hacky/errors/log-and-swallow.md +65 -0
- package/rules/ts/hacky/errors/throw-typed-error-with-context.md +64 -0
- package/rules/ts/hacky/operators/nullish-coalescing-over-or.md +50 -0
- package/rules/ts/hacky/state/exported-let.md +60 -0
- package/rules/ts/hacky/type-safety/no-double-assertion.md +34 -0
- package/rules/ts/hacky/type-safety/no-explicit-any.md +48 -0
- package/rules/ts/hacky/type-safety/no-non-null-assertion.md +43 -0
- package/rules/ts/hacky/type-safety/no-unchecked-type-assertion.md +54 -0
- package/rules/ts/hacky/type-safety/validate-parsed-json.md +48 -0
- package/rules/ts/idiom/async/no-promise-chains.md +45 -0
- package/rules/ts/idiom/async/parallelize-independent-awaits.md +46 -0
- package/rules/ts/idiom/comments/doc-comment-above-declaration.md +43 -0
- package/rules/ts/idiom/constants/as-const-for-literal-config.md +65 -0
- package/rules/ts/idiom/constants/no-enum.md +37 -0
- package/rules/ts/idiom/control-flow/always-brace-if.md +56 -0
- package/rules/ts/idiom/control-flow/guard-clauses-early-return.md +52 -0
- package/rules/ts/idiom/control-flow/no-nested-ternary.md +54 -0
- package/rules/ts/idiom/errors/error-message-lowercase.md +56 -0
- package/rules/ts/idiom/errors/log-at-boundary-not-every-layer.md +66 -0
- package/rules/ts/idiom/exports/inline-export-at-declaration.md +49 -0
- package/rules/ts/idiom/exports/no-default-export.md +37 -0
- package/rules/ts/idiom/formatting/multiline-object-literals.md +46 -0
- package/rules/ts/idiom/functions/arrow-for-callbacks.md +44 -0
- package/rules/ts/idiom/functions/function-declaration-for-top-level.md +56 -0
- package/rules/ts/idiom/imports/export-type-for-types.md +34 -0
- package/rules/ts/idiom/imports/import-order.md +66 -0
- package/rules/ts/idiom/imports/import-type-for-types.md +50 -0
- package/rules/ts/idiom/imports/side-effect-imports-last.md +42 -0
- package/rules/ts/idiom/logging/structured-logging-lowercase.md +45 -0
- package/rules/ts/idiom/naming/boolean-name-question-prefix.md +42 -0
- package/rules/ts/idiom/naming/descriptive-identifier-quality.md +48 -0
- package/rules/ts/idiom/naming/screaming-snake-module-constants.md +58 -0
- package/rules/ts/idiom/strings/template-literals-over-concat.md +51 -0
- package/rules/ts/idiom/types/interface-extends-over-intersection.md +44 -0
- package/rules/ts/idiom/types/null-vs-undefined-convention.md +43 -0
- package/rules/ts/idiom/types/prefer-type-over-interface.md +64 -0
- package/rules/ts/idiom/types/satisfies-over-annotation.md +43 -0
- package/rules/ts/idiom/types/type-predicates-for-narrowing.md +53 -0
- package/rules/ts/idiom/variables/const-by-default.md +41 -0
- package/rules/ts/idiom/variables/no-var.md +36 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
3
|
+
ignore: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
4
|
+
rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
5
|
+
failThreshold: z.ZodDefault<z.ZodNumber>;
|
|
6
|
+
scoring: z.ZodPrefault<z.ZodObject<{
|
|
7
|
+
points: z.ZodPrefault<z.ZodObject<{
|
|
8
|
+
info: z.ZodDefault<z.ZodNumber>;
|
|
9
|
+
minor: z.ZodDefault<z.ZodNumber>;
|
|
10
|
+
major: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
critical: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
}, z.core.$strict>>;
|
|
13
|
+
damping: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
densityScale: z.ZodDefault<z.ZodNumber>;
|
|
15
|
+
axisWeights: z.ZodPrefault<z.ZodObject<{
|
|
16
|
+
idiom: z.ZodDefault<z.ZodNumber>;
|
|
17
|
+
hacky: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
futureproof: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
}, z.core.$strict>>;
|
|
20
|
+
minScoredLines: z.ZodDefault<z.ZodNumber>;
|
|
21
|
+
}, z.core.$strict>>;
|
|
22
|
+
judge: z.ZodPrefault<z.ZodObject<{
|
|
23
|
+
model: z.ZodDefault<z.ZodString>;
|
|
24
|
+
maxInputTokens: z.ZodDefault<z.ZodNumber>;
|
|
25
|
+
minChangedLines: z.ZodDefault<z.ZodNumber>;
|
|
26
|
+
chunkTokens: z.ZodDefault<z.ZodNumber>;
|
|
27
|
+
minConfidence: z.ZodDefault<z.ZodNumber>;
|
|
28
|
+
maxOutputTokens: z.ZodDefault<z.ZodNumber>;
|
|
29
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
30
|
+
}, z.core.$strict>>;
|
|
31
|
+
}, z.core.$strict>, z.ZodTransform<{
|
|
32
|
+
ignore: string[];
|
|
33
|
+
rules: Record<string, boolean>;
|
|
34
|
+
failThreshold: number;
|
|
35
|
+
scoring: {
|
|
36
|
+
points: {
|
|
37
|
+
info: number;
|
|
38
|
+
minor: number;
|
|
39
|
+
major: number;
|
|
40
|
+
critical: number;
|
|
41
|
+
};
|
|
42
|
+
damping: number;
|
|
43
|
+
densityScale: number;
|
|
44
|
+
axisWeights: {
|
|
45
|
+
idiom: number;
|
|
46
|
+
hacky: number;
|
|
47
|
+
futureproof: number;
|
|
48
|
+
};
|
|
49
|
+
minScoredLines: number;
|
|
50
|
+
};
|
|
51
|
+
judge: {
|
|
52
|
+
model: string;
|
|
53
|
+
maxInputTokens: number;
|
|
54
|
+
minChangedLines: number;
|
|
55
|
+
chunkTokens: number;
|
|
56
|
+
minConfidence: number;
|
|
57
|
+
maxOutputTokens: number;
|
|
58
|
+
timeoutMs: number;
|
|
59
|
+
};
|
|
60
|
+
}, {
|
|
61
|
+
ignore: string[];
|
|
62
|
+
rules: Record<string, boolean>;
|
|
63
|
+
failThreshold: number;
|
|
64
|
+
scoring: {
|
|
65
|
+
points: {
|
|
66
|
+
info: number;
|
|
67
|
+
minor: number;
|
|
68
|
+
major: number;
|
|
69
|
+
critical: number;
|
|
70
|
+
};
|
|
71
|
+
damping: number;
|
|
72
|
+
densityScale: number;
|
|
73
|
+
axisWeights: {
|
|
74
|
+
idiom: number;
|
|
75
|
+
hacky: number;
|
|
76
|
+
futureproof: number;
|
|
77
|
+
};
|
|
78
|
+
minScoredLines: number;
|
|
79
|
+
};
|
|
80
|
+
judge: {
|
|
81
|
+
model: string;
|
|
82
|
+
maxInputTokens: number;
|
|
83
|
+
minChangedLines: number;
|
|
84
|
+
chunkTokens: number;
|
|
85
|
+
minConfidence: number;
|
|
86
|
+
maxOutputTokens: number;
|
|
87
|
+
timeoutMs: number;
|
|
88
|
+
};
|
|
89
|
+
}>>;
|
|
90
|
+
export type Config = z.infer<typeof configSchema>;
|
|
91
|
+
export declare function loadConfig(cwd: string, explicitPath?: string): Promise<Config>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { judgeConfigSchema } from './analyzers/judge.js';
|
|
6
|
+
import { scoringSchema } from './score.js';
|
|
7
|
+
import { ConfigError } from './shared/errors.js';
|
|
8
|
+
const CONFIG_FILE = '.slopscan.yml';
|
|
9
|
+
// generated and vendored code is never the author's slop
|
|
10
|
+
const DEFAULT_IGNORE = [
|
|
11
|
+
'**/node_modules/**',
|
|
12
|
+
'**/dist/**',
|
|
13
|
+
'**/vendor/**',
|
|
14
|
+
'**/*.d.ts',
|
|
15
|
+
'**/*.pb.go',
|
|
16
|
+
'**/*_gen.go',
|
|
17
|
+
'**/*.generated.*',
|
|
18
|
+
'**/pnpm-lock.yaml',
|
|
19
|
+
];
|
|
20
|
+
export const configSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
// added to the defaults, not replacing them
|
|
23
|
+
ignore: z.array(z.string()).default([]),
|
|
24
|
+
// rule id -> enabled; anything not listed is enabled
|
|
25
|
+
rules: z.record(z.string(), z.boolean()).default({}),
|
|
26
|
+
failThreshold: z.number().min(0).max(100).default(45),
|
|
27
|
+
scoring: scoringSchema.prefault({}),
|
|
28
|
+
judge: judgeConfigSchema.prefault({}),
|
|
29
|
+
})
|
|
30
|
+
.strict()
|
|
31
|
+
.transform(config => ({
|
|
32
|
+
...config,
|
|
33
|
+
ignore: [...DEFAULT_IGNORE, ...config.ignore],
|
|
34
|
+
}));
|
|
35
|
+
// reads .slopscan.yml from cwd, or the given path. a missing default file means defaults; a missing
|
|
36
|
+
// explicit file is a mistake.
|
|
37
|
+
export async function loadConfig(cwd, explicitPath) {
|
|
38
|
+
const path = resolve(cwd, explicitPath ?? CONFIG_FILE);
|
|
39
|
+
const raw = await readConfigFile(path, explicitPath !== undefined);
|
|
40
|
+
const result = configSchema.safeParse(raw);
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
const issues = result.error.issues.map(issue => ` - ${issue.path.join('.')}: ${issue.message}`);
|
|
43
|
+
throw new ConfigError([`invalid config at ${path}:`, ...issues].join('\n'));
|
|
44
|
+
}
|
|
45
|
+
return result.data;
|
|
46
|
+
}
|
|
47
|
+
async function readConfigFile(path, required) {
|
|
48
|
+
try {
|
|
49
|
+
return parseYaml(await readFile(path, 'utf-8')) ?? {};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!required && isMissingFile(error)) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
throw new ConfigError(`reading ${path}`, { cause: error });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isMissingFile(error) {
|
|
59
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Finding } from './finding.js';
|
|
2
|
+
import type { Rule } from './rule.js';
|
|
3
|
+
export type ConfirmResult = {
|
|
4
|
+
kept: Finding[];
|
|
5
|
+
rejected: Finding[];
|
|
6
|
+
};
|
|
7
|
+
export type ConfirmOptions = {
|
|
8
|
+
repo: string;
|
|
9
|
+
ignore: readonly string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function confirm(findings: readonly Finding[], rules: readonly Rule[], options: ConfirmOptions): Promise<ConfirmResult>;
|
package/dist/confirm.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { parseSource } from './analyzers/ast.js';
|
|
6
|
+
import { isIgnored } from './change.js';
|
|
7
|
+
import { Lang, detectLang } from './lang.js';
|
|
8
|
+
import { Confirm, Detect } from './rule.js';
|
|
9
|
+
import { mapConcurrent } from './shared/concurrency.js';
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
// judge findings for rules with a `confirm` predicate must pass a deterministic repo-wide check, or they are
|
|
12
|
+
// dropped. a finding without a nominated symbol cannot be checked and is dropped too.
|
|
13
|
+
export async function confirm(findings, rules, options) {
|
|
14
|
+
const predicateByRule = new Map(rules.flatMap(rule => (rule.detect === Detect.JUDGE && rule.confirm ? [[rule.id, rule.confirm]] : [])));
|
|
15
|
+
const kept = [];
|
|
16
|
+
const rejected = [];
|
|
17
|
+
const repo = new RepoIndex(options);
|
|
18
|
+
for (const finding of findings) {
|
|
19
|
+
const predicate = predicateByRule.get(finding.ruleId);
|
|
20
|
+
if (predicate === undefined) {
|
|
21
|
+
kept.push(finding);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const lang = detectLang(finding.path);
|
|
25
|
+
const passes = finding.symbol !== undefined &&
|
|
26
|
+
lang !== undefined &&
|
|
27
|
+
(await PREDICATES[predicate](repo, lang, finding.symbol));
|
|
28
|
+
(passes ? kept : rejected).push(finding);
|
|
29
|
+
}
|
|
30
|
+
return { kept, rejected };
|
|
31
|
+
}
|
|
32
|
+
// each predicate says whether the symbol is used little enough for the finding to stand
|
|
33
|
+
const PREDICATES = {
|
|
34
|
+
[Confirm.CALL_COUNT]: async (repo, lang, symbol) => (await repo.count(lang, callMatcher(symbol))) <= 1,
|
|
35
|
+
[Confirm.IMPL_COUNT]: async (repo, lang, symbol) => (await repo.count(lang, implMatcher(lang, symbol))) <= 1,
|
|
36
|
+
[Confirm.REF_COUNT]: async (repo, lang, symbol) => (await repo.count(lang, refMatcher(lang, symbol))) <= 1,
|
|
37
|
+
};
|
|
38
|
+
// call sites: `symbol(...)` or `anything.symbol(...)` in either language
|
|
39
|
+
function callMatcher(symbol) {
|
|
40
|
+
return {
|
|
41
|
+
rule: {
|
|
42
|
+
kind: 'call_expression',
|
|
43
|
+
has: {
|
|
44
|
+
field: 'function',
|
|
45
|
+
regex: `(^|\\.)${escape(symbol)}$`,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// TS: classes that implement the interface. Go: compile-time assertions `var _ Iface = ...`
|
|
51
|
+
function implMatcher(lang, symbol) {
|
|
52
|
+
const pattern = `\\b${escape(symbol)}\\b`;
|
|
53
|
+
return lang === Lang.TS
|
|
54
|
+
? { rule: { kind: 'implements_clause', regex: pattern } }
|
|
55
|
+
: { rule: { kind: 'var_spec', regex: `^_\\s+${escape(symbol)}\\b` } };
|
|
56
|
+
}
|
|
57
|
+
// every identifier spelled exactly like the symbol; the definition counts once
|
|
58
|
+
function refMatcher(lang, symbol) {
|
|
59
|
+
const kinds = lang === Lang.TS
|
|
60
|
+
? ['identifier', 'type_identifier', 'property_identifier']
|
|
61
|
+
: ['identifier', 'field_identifier', 'type_identifier'];
|
|
62
|
+
return {
|
|
63
|
+
rule: {
|
|
64
|
+
any: kinds.map(kind => ({ kind, regex: `^${escape(symbol)}$` })),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// parses each tracked file of a language once and answers count queries against the trees
|
|
69
|
+
class RepoIndex {
|
|
70
|
+
#options;
|
|
71
|
+
#trees = new Map();
|
|
72
|
+
constructor(options) {
|
|
73
|
+
this.#options = options;
|
|
74
|
+
}
|
|
75
|
+
async count(lang, matcher) {
|
|
76
|
+
const trees = await this.#load(lang);
|
|
77
|
+
return trees.reduce((sum, tree) => sum + tree.findAll(matcher).length, 0);
|
|
78
|
+
}
|
|
79
|
+
#load(lang) {
|
|
80
|
+
let trees = this.#trees.get(lang);
|
|
81
|
+
if (trees === undefined) {
|
|
82
|
+
trees = this.#parseAll(lang);
|
|
83
|
+
this.#trees.set(lang, trees);
|
|
84
|
+
}
|
|
85
|
+
return trees;
|
|
86
|
+
}
|
|
87
|
+
async #parseAll(lang) {
|
|
88
|
+
const paths = (await trackedFiles(this.#options.repo)).filter(path => detectLang(path) === lang && !isIgnored(path, this.#options.ignore));
|
|
89
|
+
return mapConcurrent(paths, async (path) => parseSource(lang, await readFile(join(this.#options.repo, path), 'utf-8'), path));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// git knows which files are source and which are ignored, so we ask it rather than walking the tree
|
|
93
|
+
async function trackedFiles(repo) {
|
|
94
|
+
const { stdout } = await execFileAsync('git', ['ls-files', '-z'], { cwd: repo, encoding: 'utf-8' });
|
|
95
|
+
return stdout.split('\0').filter(path => path.length > 0);
|
|
96
|
+
}
|
|
97
|
+
function escape(text) {
|
|
98
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=confirm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirm.js","sourceRoot":"","sources":["../src/confirm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const findingSchema: z.ZodObject<{
|
|
3
|
+
ruleId: z.ZodString;
|
|
4
|
+
path: z.ZodString;
|
|
5
|
+
line: z.ZodNumber;
|
|
6
|
+
endLine: z.ZodNumber;
|
|
7
|
+
quote: z.ZodString;
|
|
8
|
+
message: z.ZodString;
|
|
9
|
+
confidence: z.ZodNumber;
|
|
10
|
+
symbol: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export type Finding = z.infer<typeof findingSchema>;
|
|
13
|
+
export declare function compareFindings(a: Finding, b: Finding): number;
|
package/dist/finding.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// one violation of one rule at one place. `quote` is verbatim source so a reader can find it and so judge
|
|
3
|
+
// findings can be checked against the file. `symbol` is a judge nomination for a confirm predicate.
|
|
4
|
+
export const findingSchema = z.object({
|
|
5
|
+
ruleId: z.string(),
|
|
6
|
+
path: z.string(),
|
|
7
|
+
line: z.number().int().positive(),
|
|
8
|
+
endLine: z.number().int().positive(),
|
|
9
|
+
quote: z.string(),
|
|
10
|
+
message: z.string(),
|
|
11
|
+
confidence: z.number().min(0).max(1),
|
|
12
|
+
symbol: z.string().optional(),
|
|
13
|
+
});
|
|
14
|
+
// a stable order for reports and snapshots: by file, then line, then rule
|
|
15
|
+
export function compareFindings(a, b) {
|
|
16
|
+
return a.path.localeCompare(b.path) || a.line - b.line || a.ruleId.localeCompare(b.ruleId);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=finding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finding.js","sourceRoot":"","sources":["../src/finding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,0GAA0G;AAC1G,oGAAoG;AACpG,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACjC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACpC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC,CAAC;AAIH,0EAA0E;AAC1E,MAAM,UAAU,eAAe,CAAC,CAAU,EAAE,CAAU;IACrD,OAAO,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5F,CAAC"}
|
package/dist/lang.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const Lang: {
|
|
2
|
+
readonly TS: "ts";
|
|
3
|
+
readonly GO: "go";
|
|
4
|
+
};
|
|
5
|
+
export type Lang = (typeof Lang)[keyof typeof Lang];
|
|
6
|
+
export declare const langValues: [Lang, ...Lang[]];
|
|
7
|
+
export declare const RuleLang: {
|
|
8
|
+
readonly ANY: "any";
|
|
9
|
+
readonly TS: "ts";
|
|
10
|
+
readonly GO: "go";
|
|
11
|
+
};
|
|
12
|
+
export type RuleLang = (typeof RuleLang)[keyof typeof RuleLang];
|
|
13
|
+
export declare const ruleLangValues: [RuleLang, ...RuleLang[]];
|
|
14
|
+
export declare function detectLang(path: string): Lang | undefined;
|
|
15
|
+
export declare function fenceLang(info: string): Lang | undefined;
|
|
16
|
+
export declare function ruleAppliesTo(ruleLang: RuleLang, lang: Lang): boolean;
|
package/dist/lang.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
export const Lang = {
|
|
3
|
+
TS: 'ts',
|
|
4
|
+
GO: 'go',
|
|
5
|
+
};
|
|
6
|
+
export const langValues = Object.values(Lang);
|
|
7
|
+
// a rule targets one language or both
|
|
8
|
+
export const RuleLang = {
|
|
9
|
+
...Lang,
|
|
10
|
+
ANY: 'any',
|
|
11
|
+
};
|
|
12
|
+
export const ruleLangValues = Object.values(RuleLang);
|
|
13
|
+
const EXTENSION_LANGS = {
|
|
14
|
+
'.ts': Lang.TS,
|
|
15
|
+
'.tsx': Lang.TS,
|
|
16
|
+
'.mts': Lang.TS,
|
|
17
|
+
'.cts': Lang.TS,
|
|
18
|
+
'.go': Lang.GO,
|
|
19
|
+
};
|
|
20
|
+
const FENCE_LANGS = {
|
|
21
|
+
ts: Lang.TS,
|
|
22
|
+
tsx: Lang.TS,
|
|
23
|
+
typescript: Lang.TS,
|
|
24
|
+
go: Lang.GO,
|
|
25
|
+
golang: Lang.GO,
|
|
26
|
+
};
|
|
27
|
+
export function detectLang(path) {
|
|
28
|
+
return EXTENSION_LANGS[extname(path)];
|
|
29
|
+
}
|
|
30
|
+
// maps a markdown fence info string (```ts, ```go) to a language
|
|
31
|
+
export function fenceLang(info) {
|
|
32
|
+
return FENCE_LANGS[info.trim().toLowerCase()];
|
|
33
|
+
}
|
|
34
|
+
export function ruleAppliesTo(ruleLang, lang) {
|
|
35
|
+
return ruleLang === RuleLang.ANY || ruleLang === lang;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=lang.js.map
|
package/dist/lang.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lang.js","sourceRoot":"","sources":["../src/lang.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,CAAC,MAAM,IAAI,GAAG;IACnB,EAAE,EAAE,IAAI;IACR,EAAE,EAAE,IAAI;CACC,CAAC;AAIX,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAsB,CAAC;AAEnE,sCAAsC;AACtC,MAAM,CAAC,MAAM,QAAQ,GAAG;IACvB,GAAG,IAAI;IACP,GAAG,EAAE,KAAK;CACD,CAAC;AAIX,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAA8B,CAAC;AAEnF,MAAM,eAAe,GAAmC;IACvD,KAAK,EAAE,IAAI,CAAC,EAAE;IACd,MAAM,EAAE,IAAI,CAAC,EAAE;IACf,MAAM,EAAE,IAAI,CAAC,EAAE;IACf,MAAM,EAAE,IAAI,CAAC,EAAE;IACf,KAAK,EAAE,IAAI,CAAC,EAAE;CACd,CAAC;AAEF,MAAM,WAAW,GAAmC;IACnD,EAAE,EAAE,IAAI,CAAC,EAAE;IACX,GAAG,EAAE,IAAI,CAAC,EAAE;IACZ,UAAU,EAAE,IAAI,CAAC,EAAE;IACnB,EAAE,EAAE,IAAI,CAAC,EAAE;IACX,MAAM,EAAE,IAAI,CAAC,EAAE;CACf,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,IAAY;IACtC,OAAO,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,SAAS,CAAC,IAAY;IACrC,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAkB,EAAE,IAAU;IAC3D,OAAO,QAAQ,KAAK,QAAQ,CAAC,GAAG,IAAI,QAAQ,KAAK,IAAI,CAAC;AACvD,CAAC"}
|
package/dist/model.d.ts
ADDED
package/dist/model.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
2
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
3
|
+
import { env } from './shared/env.js';
|
|
4
|
+
import { ConfigError } from './shared/errors.js';
|
|
5
|
+
const Provider = {
|
|
6
|
+
OPENAI: 'openai',
|
|
7
|
+
OPENROUTER: 'openrouter',
|
|
8
|
+
};
|
|
9
|
+
const providerValues = Object.values(Provider);
|
|
10
|
+
// resolves "provider/model-id" from config to an AI SDK model, asking for the one key it needs
|
|
11
|
+
export function resolveModel(spec) {
|
|
12
|
+
const slash = spec.indexOf('/');
|
|
13
|
+
const provider = spec.slice(0, slash);
|
|
14
|
+
const modelId = spec.slice(slash + 1);
|
|
15
|
+
if (slash === -1 || !isProvider(provider) || modelId.length === 0) {
|
|
16
|
+
throw new ConfigError(`judge.model must be "<provider>/<model>" with provider one of ${providerValues.join(', ')}`);
|
|
17
|
+
}
|
|
18
|
+
switch (provider) {
|
|
19
|
+
case Provider.OPENAI:
|
|
20
|
+
return createOpenAI({ apiKey: requireKey('OPENAI_API_KEY', env.OPENAI_API_KEY) })(modelId);
|
|
21
|
+
case Provider.OPENROUTER:
|
|
22
|
+
return createOpenRouter({ apiKey: requireKey('OPENROUTER_API_KEY', env.OPENROUTER_API_KEY) })(modelId);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function isProvider(value) {
|
|
26
|
+
return providerValues.includes(value);
|
|
27
|
+
}
|
|
28
|
+
function requireKey(name, value) {
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
throw new ConfigError(`${name} is required for the judge; set it or run with --no-judge`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,QAAQ,GAAG;IAChB,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,YAAY;CACf,CAAC;AAIX,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAsB,CAAC;AAEpE,+FAA+F;AAC/F,MAAM,UAAU,YAAY,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAEtC,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,WAAW,CACpB,iEAAiE,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC5F,CAAC;IACH,CAAC;IAED,QAAQ,QAAQ,EAAE,CAAC;QAClB,KAAK,QAAQ,CAAC,MAAM;YACnB,OAAO,YAAY,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,gBAAgB,EAAE,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QAC5F,KAAK,QAAQ,CAAC,UAAU;YACvB,OAAO,gBAAgB,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,oBAAoB,EAAE,GAAG,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IACzG,CAAC;AACF,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAChC,OAAO,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,UAAU,CAAC,IAAY,EAAE,KAAyB;IAC1D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,2DAA2D,CAAC,CAAC;IAC3F,CAAC;IAED,OAAO,KAAK,CAAC;AACd,CAAC"}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const reportSchema: z.ZodObject<{
|
|
3
|
+
overall: z.ZodNumber;
|
|
4
|
+
grade: z.ZodEnum<{
|
|
5
|
+
A: "A";
|
|
6
|
+
B: "B";
|
|
7
|
+
C: "C";
|
|
8
|
+
D: "D";
|
|
9
|
+
F: "F";
|
|
10
|
+
}>;
|
|
11
|
+
axes: z.ZodObject<{
|
|
12
|
+
idiom: z.ZodObject<{
|
|
13
|
+
score: z.ZodNumber;
|
|
14
|
+
points: z.ZodNumber;
|
|
15
|
+
density: z.ZodNumber;
|
|
16
|
+
findings: z.ZodNumber;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
hacky: z.ZodObject<{
|
|
19
|
+
score: z.ZodNumber;
|
|
20
|
+
points: z.ZodNumber;
|
|
21
|
+
density: z.ZodNumber;
|
|
22
|
+
findings: z.ZodNumber;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
futureproof: z.ZodObject<{
|
|
25
|
+
score: z.ZodNumber;
|
|
26
|
+
points: z.ZodNumber;
|
|
27
|
+
density: z.ZodNumber;
|
|
28
|
+
findings: z.ZodNumber;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
files: z.ZodNumber;
|
|
32
|
+
scoredLines: z.ZodNumber;
|
|
33
|
+
findings: z.ZodArray<z.ZodObject<{
|
|
34
|
+
ruleId: z.ZodString;
|
|
35
|
+
path: z.ZodString;
|
|
36
|
+
line: z.ZodNumber;
|
|
37
|
+
endLine: z.ZodNumber;
|
|
38
|
+
quote: z.ZodString;
|
|
39
|
+
message: z.ZodString;
|
|
40
|
+
confidence: z.ZodNumber;
|
|
41
|
+
symbol: z.ZodOptional<z.ZodString>;
|
|
42
|
+
axis: z.ZodEnum<{
|
|
43
|
+
idiom: "idiom";
|
|
44
|
+
hacky: "hacky";
|
|
45
|
+
futureproof: "futureproof";
|
|
46
|
+
}>;
|
|
47
|
+
severity: z.ZodEnum<{
|
|
48
|
+
info: "info";
|
|
49
|
+
minor: "minor";
|
|
50
|
+
major: "major";
|
|
51
|
+
critical: "critical";
|
|
52
|
+
}>;
|
|
53
|
+
points: z.ZodNumber;
|
|
54
|
+
}, z.core.$strip>>;
|
|
55
|
+
baseline: z.ZodOptional<z.ZodObject<{
|
|
56
|
+
overall: z.ZodNumber;
|
|
57
|
+
grade: z.ZodEnum<{
|
|
58
|
+
A: "A";
|
|
59
|
+
B: "B";
|
|
60
|
+
C: "C";
|
|
61
|
+
D: "D";
|
|
62
|
+
F: "F";
|
|
63
|
+
}>;
|
|
64
|
+
axes: z.ZodObject<{
|
|
65
|
+
idiom: z.ZodObject<{
|
|
66
|
+
score: z.ZodNumber;
|
|
67
|
+
points: z.ZodNumber;
|
|
68
|
+
density: z.ZodNumber;
|
|
69
|
+
findings: z.ZodNumber;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
hacky: z.ZodObject<{
|
|
72
|
+
score: z.ZodNumber;
|
|
73
|
+
points: z.ZodNumber;
|
|
74
|
+
density: z.ZodNumber;
|
|
75
|
+
findings: z.ZodNumber;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
futureproof: z.ZodObject<{
|
|
78
|
+
score: z.ZodNumber;
|
|
79
|
+
points: z.ZodNumber;
|
|
80
|
+
density: z.ZodNumber;
|
|
81
|
+
findings: z.ZodNumber;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
}, z.core.$strip>;
|
|
84
|
+
}, z.core.$strip>>;
|
|
85
|
+
judge: z.ZodOptional<z.ZodObject<{
|
|
86
|
+
model: z.ZodString;
|
|
87
|
+
promptVersion: z.ZodString;
|
|
88
|
+
calls: z.ZodNumber;
|
|
89
|
+
cachedCalls: z.ZodNumber;
|
|
90
|
+
inputTokens: z.ZodNumber;
|
|
91
|
+
outputTokens: z.ZodNumber;
|
|
92
|
+
skipped: z.ZodArray<z.ZodObject<{
|
|
93
|
+
path: z.ZodString;
|
|
94
|
+
reason: z.ZodEnum<{
|
|
95
|
+
budget: "budget";
|
|
96
|
+
"too-small": "too-small";
|
|
97
|
+
}>;
|
|
98
|
+
}, z.core.$strip>>;
|
|
99
|
+
}, z.core.$strip>>;
|
|
100
|
+
rejected: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
101
|
+
ruleId: z.ZodString;
|
|
102
|
+
path: z.ZodString;
|
|
103
|
+
line: z.ZodNumber;
|
|
104
|
+
endLine: z.ZodNumber;
|
|
105
|
+
quote: z.ZodString;
|
|
106
|
+
message: z.ZodString;
|
|
107
|
+
confidence: z.ZodNumber;
|
|
108
|
+
symbol: z.ZodOptional<z.ZodString>;
|
|
109
|
+
}, z.core.$strip>>>;
|
|
110
|
+
}, z.core.$strip>;
|
|
111
|
+
export type Report = z.infer<typeof reportSchema>;
|
|
112
|
+
export declare const Format: {
|
|
113
|
+
readonly TERMINAL: "terminal";
|
|
114
|
+
readonly JSON: "json";
|
|
115
|
+
readonly MARKDOWN: "markdown";
|
|
116
|
+
};
|
|
117
|
+
export type Format = (typeof Format)[keyof typeof Format];
|
|
118
|
+
export declare const formatValues: [Format, ...Format[]];
|
|
119
|
+
export declare function isFormat(value: string): value is Format;
|
|
120
|
+
export type RenderOptions = {
|
|
121
|
+
format: Format;
|
|
122
|
+
color: boolean;
|
|
123
|
+
};
|
|
124
|
+
export declare function renderReport(report: Report, options: RenderOptions): string;
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { judgeSummarySchema } from './analyzers/judge.js';
|
|
4
|
+
import { findingSchema } from './finding.js';
|
|
5
|
+
import { axisValues } from './rule.js';
|
|
6
|
+
import { Grade, scoreSchema, scoredSchema } from './score.js';
|
|
7
|
+
// what one scan produces: the score, its findings, and how it was made
|
|
8
|
+
export const reportSchema = scoredSchema.extend({
|
|
9
|
+
baseline: scoreSchema.optional(),
|
|
10
|
+
judge: judgeSummarySchema.optional(),
|
|
11
|
+
// judge findings that failed their confirm predicate; kept for tuning, hidden from humans
|
|
12
|
+
rejected: z.array(findingSchema).optional(),
|
|
13
|
+
});
|
|
14
|
+
export const Format = {
|
|
15
|
+
TERMINAL: 'terminal',
|
|
16
|
+
JSON: 'json',
|
|
17
|
+
MARKDOWN: 'markdown',
|
|
18
|
+
};
|
|
19
|
+
export const formatValues = Object.values(Format);
|
|
20
|
+
export function isFormat(value) {
|
|
21
|
+
return formatValues.includes(value);
|
|
22
|
+
}
|
|
23
|
+
// a pull request comment is read top to bottom; the ranked head is what matters, the tail lives in the json
|
|
24
|
+
const MARKDOWN_MAX_FINDINGS = 30;
|
|
25
|
+
export function renderReport(report, options) {
|
|
26
|
+
switch (options.format) {
|
|
27
|
+
case Format.TERMINAL:
|
|
28
|
+
return renderTerminal(report, options.color);
|
|
29
|
+
case Format.JSON:
|
|
30
|
+
return renderJson(report);
|
|
31
|
+
case Format.MARKDOWN:
|
|
32
|
+
return renderMarkdown(report);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const GRADE_COLOR = {
|
|
36
|
+
[Grade.A]: pc.green,
|
|
37
|
+
[Grade.B]: pc.green,
|
|
38
|
+
[Grade.C]: pc.yellow,
|
|
39
|
+
[Grade.D]: pc.red,
|
|
40
|
+
[Grade.F]: pc.red,
|
|
41
|
+
};
|
|
42
|
+
function renderTerminal(report, color) {
|
|
43
|
+
const paint = color ? GRADE_COLOR[report.grade] : identity;
|
|
44
|
+
const dim = color ? pc.dim : identity;
|
|
45
|
+
const bold = color ? pc.bold : identity;
|
|
46
|
+
const lines = [
|
|
47
|
+
`${bold('slop score')} ${paint(`${report.overall} (${report.grade})`)}${deltaSuffix(report)}`,
|
|
48
|
+
axisValues.map(axis => `${axis} ${Math.round(report.axes[axis].score)}`).join(dim(' · ')),
|
|
49
|
+
dim(`${report.files} files, ${report.scoredLines} scored lines`),
|
|
50
|
+
];
|
|
51
|
+
if (report.judge !== undefined) {
|
|
52
|
+
lines.push(dim(judgeLine(report.judge)));
|
|
53
|
+
}
|
|
54
|
+
lines.push('');
|
|
55
|
+
if (report.findings.length === 0) {
|
|
56
|
+
lines.push(dim('no findings'));
|
|
57
|
+
return `${lines.join('\n')}\n`;
|
|
58
|
+
}
|
|
59
|
+
for (const finding of report.findings) {
|
|
60
|
+
lines.push(`${bold(`${finding.path}:${finding.line}`)} ${dim(`[${finding.severity}]`)} ${finding.ruleId}`, ` ${finding.message}`, ` ${dim(`> ${finding.quote}`)}`, '');
|
|
61
|
+
}
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
function renderJson(report) {
|
|
65
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
66
|
+
}
|
|
67
|
+
function renderMarkdown(report) {
|
|
68
|
+
const lines = [
|
|
69
|
+
`## Slop score: ${report.overall} (${report.grade})${deltaSuffix(report)}`,
|
|
70
|
+
'',
|
|
71
|
+
'| axis | score | findings |',
|
|
72
|
+
'| --- | ---: | ---: |',
|
|
73
|
+
...axisValues.map(axis => `| ${axis} | ${Math.round(report.axes[axis].score)} | ${report.axes[axis].findings} |`),
|
|
74
|
+
'',
|
|
75
|
+
`${report.files} files, ${report.scoredLines} scored lines.${report.judge ? ` ${judgeLine(report.judge)}.` : ''}`,
|
|
76
|
+
'',
|
|
77
|
+
];
|
|
78
|
+
if (report.findings.length === 0) {
|
|
79
|
+
lines.push('No findings.', '');
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
82
|
+
lines.push('| location | severity | rule | message |', '| --- | --- | --- | --- |');
|
|
83
|
+
for (const finding of report.findings.slice(0, MARKDOWN_MAX_FINDINGS)) {
|
|
84
|
+
lines.push(`| \`${finding.path}:${finding.line}\` | ${finding.severity} | \`${finding.ruleId}\` | ${escapePipes(finding.message)} |`);
|
|
85
|
+
}
|
|
86
|
+
const hidden = report.findings.length - MARKDOWN_MAX_FINDINGS;
|
|
87
|
+
if (hidden > 0) {
|
|
88
|
+
lines.push('', `${hidden} more findings in the JSON report.`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
function judgeLine(judge) {
|
|
94
|
+
const skipped = judge.skipped.length > 0 ? `, ${judge.skipped.length} files skipped` : '';
|
|
95
|
+
return `judge ${judge.model} (${judge.promptVersion}): ${judge.calls} calls, ${judge.cachedCalls} cached, ${judge.inputTokens} in / ${judge.outputTokens} out${skipped}`;
|
|
96
|
+
}
|
|
97
|
+
// "39 (C) was 45 (C), -6" when a baseline is present
|
|
98
|
+
function deltaSuffix(report) {
|
|
99
|
+
if (report.baseline === undefined) {
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
const delta = report.overall - report.baseline.overall;
|
|
103
|
+
const sign = delta > 0 ? '+' : '';
|
|
104
|
+
return ` was ${report.baseline.overall} (${report.baseline.grade}), ${sign}${delta}`;
|
|
105
|
+
}
|
|
106
|
+
function escapePipes(text) {
|
|
107
|
+
return text.replaceAll('|', '\\|');
|
|
108
|
+
}
|
|
109
|
+
function identity(text) {
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=report.js.map
|