@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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// how far the model's reported line may drift from where the quote is actually found
|
|
3
|
+
const LINE_TOLERANCE = 3;
|
|
4
|
+
const MAX_QUOTE_LENGTH = 400;
|
|
5
|
+
const MAX_MESSAGE_LENGTH = 240;
|
|
6
|
+
export function judgeOutputSchema(ruleIds) {
|
|
7
|
+
return z.object({
|
|
8
|
+
findings: z.array(z.object({
|
|
9
|
+
ruleId: z.enum(ruleIds),
|
|
10
|
+
line: z.number().int().positive(),
|
|
11
|
+
quote: z.string().min(1).max(MAX_QUOTE_LENGTH),
|
|
12
|
+
message: z.string().max(MAX_MESSAGE_LENGTH),
|
|
13
|
+
confidence: z.number().min(0).max(1),
|
|
14
|
+
symbol: z.string().nullable(),
|
|
15
|
+
})),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
// keeps only findings whose quote really appears in the file, on a changed line, near the reported line,
|
|
19
|
+
// with enough confidence. one survivor per rule per span.
|
|
20
|
+
export function validateFindings(raw, change, rules, minConfidence) {
|
|
21
|
+
const lines = change.source.split('\n');
|
|
22
|
+
const normalized = lines.map(normalize);
|
|
23
|
+
const located = raw.flatMap(candidate => {
|
|
24
|
+
const rule = rules.get(candidate.ruleId);
|
|
25
|
+
if (rule === undefined || candidate.confidence < minConfidence) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const line = locate(candidate, normalized);
|
|
29
|
+
if (line === undefined || !change.changedLines.has(line)) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const quoteLines = candidate.quote.split('\n').length;
|
|
33
|
+
const symbol = candidate.symbol?.trim();
|
|
34
|
+
const finding = {
|
|
35
|
+
ruleId: candidate.ruleId,
|
|
36
|
+
path: change.path,
|
|
37
|
+
line,
|
|
38
|
+
endLine: Math.min(lines.length, line + quoteLines - 1),
|
|
39
|
+
quote: lines.slice(line - 1, line - 1 + quoteLines).join('\n'),
|
|
40
|
+
message: candidate.message.trim() || rule.message,
|
|
41
|
+
confidence: candidate.confidence,
|
|
42
|
+
...(symbol && { symbol }),
|
|
43
|
+
};
|
|
44
|
+
return [finding];
|
|
45
|
+
});
|
|
46
|
+
return dedupe(located);
|
|
47
|
+
}
|
|
48
|
+
// the line whose text contains the quote's first line, closest to where the model said it was
|
|
49
|
+
function locate(candidate, normalizedLines) {
|
|
50
|
+
const needle = normalize(candidate.quote.split('\n')[0] ?? '');
|
|
51
|
+
if (needle.length === 0) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
let best;
|
|
55
|
+
for (let offset = 0; offset <= LINE_TOLERANCE; offset += 1) {
|
|
56
|
+
for (const line of [candidate.line - offset, candidate.line + offset]) {
|
|
57
|
+
if (normalizedLines[line - 1]?.includes(needle)) {
|
|
58
|
+
best ??= line;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (best !== undefined) {
|
|
62
|
+
return best;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
// same rule, overlapping span: keep the most confident
|
|
68
|
+
function dedupe(findings) {
|
|
69
|
+
const kept = [];
|
|
70
|
+
for (const finding of [...findings].sort((a, b) => b.confidence - a.confidence)) {
|
|
71
|
+
const overlaps = kept.some(other => other.ruleId === finding.ruleId && other.line <= finding.endLine && finding.line <= other.endLine);
|
|
72
|
+
if (!overlaps) {
|
|
73
|
+
kept.push(finding);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return kept;
|
|
77
|
+
}
|
|
78
|
+
function normalize(text) {
|
|
79
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=judge-validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"judge-validate.js","sourceRoot":"","sources":["../../src/analyzers/judge-validate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { LanguageModel } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { Analyzer } from '../analyzer.js';
|
|
4
|
+
export declare const judgeConfigSchema: z.ZodObject<{
|
|
5
|
+
model: z.ZodDefault<z.ZodString>;
|
|
6
|
+
maxInputTokens: z.ZodDefault<z.ZodNumber>;
|
|
7
|
+
minChangedLines: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
chunkTokens: z.ZodDefault<z.ZodNumber>;
|
|
9
|
+
minConfidence: z.ZodDefault<z.ZodNumber>;
|
|
10
|
+
maxOutputTokens: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
}, z.core.$strict>;
|
|
13
|
+
export type JudgeConfig = z.infer<typeof judgeConfigSchema>;
|
|
14
|
+
export declare const SkipReason: {
|
|
15
|
+
readonly BUDGET: "budget";
|
|
16
|
+
readonly TOO_SMALL: "too-small";
|
|
17
|
+
};
|
|
18
|
+
export type SkipReason = (typeof SkipReason)[keyof typeof SkipReason];
|
|
19
|
+
export declare const judgeSummarySchema: z.ZodObject<{
|
|
20
|
+
model: z.ZodString;
|
|
21
|
+
promptVersion: z.ZodString;
|
|
22
|
+
calls: z.ZodNumber;
|
|
23
|
+
cachedCalls: z.ZodNumber;
|
|
24
|
+
inputTokens: z.ZodNumber;
|
|
25
|
+
outputTokens: z.ZodNumber;
|
|
26
|
+
skipped: z.ZodArray<z.ZodObject<{
|
|
27
|
+
path: z.ZodString;
|
|
28
|
+
reason: z.ZodEnum<{
|
|
29
|
+
budget: "budget";
|
|
30
|
+
"too-small": "too-small";
|
|
31
|
+
}>;
|
|
32
|
+
}, z.core.$strip>>;
|
|
33
|
+
}, z.core.$strip>;
|
|
34
|
+
export type JudgeSummary = z.infer<typeof judgeSummarySchema>;
|
|
35
|
+
export type Judge = {
|
|
36
|
+
analyze: Analyzer;
|
|
37
|
+
summary(): JudgeSummary;
|
|
38
|
+
};
|
|
39
|
+
export type JudgeOptions = {
|
|
40
|
+
config: JudgeConfig;
|
|
41
|
+
model: LanguageModel;
|
|
42
|
+
cacheDir: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function createJudge(options: JudgeOptions): Judge;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { Output, generateText } from 'ai';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { Detect } from '../rule.js';
|
|
7
|
+
import { logger } from '../shared/log.js';
|
|
8
|
+
import { loadPrompt } from '../shared/prompts.js';
|
|
9
|
+
import { chunkChange } from './judge-chunks.js';
|
|
10
|
+
import { judgeOutputSchema, validateFindings } from './judge-validate.js';
|
|
11
|
+
export const judgeConfigSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
// "<provider>/<model id>"; see model.ts for providers
|
|
14
|
+
model: z.string().default('openai/gpt-6-sol'),
|
|
15
|
+
// total prompt tokens one run may spend before it starts skipping files
|
|
16
|
+
maxInputTokens: z.number().int().positive().default(500_000),
|
|
17
|
+
// files with fewer changed lines than this skip the judge
|
|
18
|
+
minChangedLines: z.number().int().positive().default(3),
|
|
19
|
+
// prompt tokens of code per model call
|
|
20
|
+
chunkTokens: z.number().int().positive().default(6000),
|
|
21
|
+
minConfidence: z.number().min(0).max(1).default(0.6),
|
|
22
|
+
maxOutputTokens: z.number().int().positive().default(2000),
|
|
23
|
+
// wall-clock cap per model call, retries included
|
|
24
|
+
timeoutMs: z.number().int().positive().default(90_000),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
export const SkipReason = {
|
|
28
|
+
BUDGET: 'budget',
|
|
29
|
+
TOO_SMALL: 'too-small',
|
|
30
|
+
};
|
|
31
|
+
export const judgeSummarySchema = z.object({
|
|
32
|
+
model: z.string(),
|
|
33
|
+
promptVersion: z.string(),
|
|
34
|
+
calls: z.number().int().nonnegative(),
|
|
35
|
+
cachedCalls: z.number().int().nonnegative(),
|
|
36
|
+
inputTokens: z.number().int().nonnegative(),
|
|
37
|
+
outputTokens: z.number().int().nonnegative(),
|
|
38
|
+
skipped: z.array(z.object({
|
|
39
|
+
path: z.string(),
|
|
40
|
+
reason: z.enum([SkipReason.BUDGET, SkipReason.TOO_SMALL]),
|
|
41
|
+
})),
|
|
42
|
+
});
|
|
43
|
+
const PROMPT_NAME = 'judge';
|
|
44
|
+
const PROMPT_VERSION_LENGTH = 12;
|
|
45
|
+
// one model call per chunk of changed code, with the judge rules for that language as the rubric.
|
|
46
|
+
// results are cached by content so the same head commit always produces the same findings.
|
|
47
|
+
export function createJudge(options) {
|
|
48
|
+
let remainingTokens = options.config.maxInputTokens;
|
|
49
|
+
const summary = {
|
|
50
|
+
model: options.config.model,
|
|
51
|
+
promptVersion: '',
|
|
52
|
+
calls: 0,
|
|
53
|
+
cachedCalls: 0,
|
|
54
|
+
inputTokens: 0,
|
|
55
|
+
outputTokens: 0,
|
|
56
|
+
skipped: [],
|
|
57
|
+
};
|
|
58
|
+
async function analyze(change, rules) {
|
|
59
|
+
const judgeRules = rules.filter((rule) => rule.detect === Detect.JUDGE);
|
|
60
|
+
const ruleIds = judgeRules.map(rule => rule.id);
|
|
61
|
+
if (!isNonEmpty(ruleIds)) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
if (change.changedLines.size < options.config.minChangedLines) {
|
|
65
|
+
summary.skipped.push({ path: change.path, reason: SkipReason.TOO_SMALL });
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const astRuleIds = rules.filter(rule => rule.detect === Detect.AST).map(rule => rule.id);
|
|
69
|
+
const system = loadPrompt(PROMPT_NAME, {
|
|
70
|
+
RULES: renderRules(judgeRules),
|
|
71
|
+
HANDLED_BY_AST: astRuleIds.length > 0 ? astRuleIds.join(', ') : 'none',
|
|
72
|
+
});
|
|
73
|
+
summary.promptVersion = hash(system).slice(0, PROMPT_VERSION_LENGTH);
|
|
74
|
+
const schema = judgeOutputSchema(ruleIds);
|
|
75
|
+
const rulesById = new Map(judgeRules.map(rule => [rule.id, rule]));
|
|
76
|
+
const raw = [];
|
|
77
|
+
for (const chunk of chunkChange(change, options.config.chunkTokens)) {
|
|
78
|
+
const estimated = chunk.estimatedTokens + estimateSystemTokens(system);
|
|
79
|
+
if (estimated > remainingTokens) {
|
|
80
|
+
summary.skipped.push({ path: change.path, reason: SkipReason.BUDGET });
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const output = await judgeChunk(chunk, system, schema);
|
|
84
|
+
raw.push(...output.findings);
|
|
85
|
+
}
|
|
86
|
+
return validateFindings(raw, change, rulesById, options.config.minConfidence);
|
|
87
|
+
}
|
|
88
|
+
async function judgeChunk(chunk, system, schema) {
|
|
89
|
+
const key = hash(`${options.config.model}\n${system}\n${chunk.text}`);
|
|
90
|
+
const cached = await readCache(options.cacheDir, key, schema);
|
|
91
|
+
if (cached !== undefined) {
|
|
92
|
+
summary.cachedCalls += 1;
|
|
93
|
+
logger.debug({ path: chunk.path, cached: true, findings: cached.findings }, 'judge output');
|
|
94
|
+
return cached;
|
|
95
|
+
}
|
|
96
|
+
const result = await generateText({
|
|
97
|
+
model: options.model,
|
|
98
|
+
output: Output.object({ schema, name: 'findings' }),
|
|
99
|
+
system,
|
|
100
|
+
prompt: chunk.text,
|
|
101
|
+
temperature: 0,
|
|
102
|
+
maxRetries: 2,
|
|
103
|
+
maxOutputTokens: options.config.maxOutputTokens,
|
|
104
|
+
timeout: options.config.timeoutMs,
|
|
105
|
+
});
|
|
106
|
+
summary.calls += 1;
|
|
107
|
+
summary.inputTokens += result.usage.inputTokens ?? 0;
|
|
108
|
+
summary.outputTokens += result.usage.outputTokens ?? 0;
|
|
109
|
+
remainingTokens -= result.usage.inputTokens ?? chunk.estimatedTokens;
|
|
110
|
+
logger.debug({ path: chunk.path, cached: false, findings: result.output.findings }, 'judge output');
|
|
111
|
+
await writeCache(options.cacheDir, key, result.output);
|
|
112
|
+
return result.output;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
analyze,
|
|
116
|
+
summary: () => structuredClone(summary),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// the rubric: id, message, why, the shortest bad and good example, and known false positives
|
|
120
|
+
function renderRules(rules) {
|
|
121
|
+
return rules
|
|
122
|
+
.map(rule => {
|
|
123
|
+
const bad = shortest(rule.bad.map(example => example.source));
|
|
124
|
+
const good = shortest(rule.good.map(example => example.source));
|
|
125
|
+
const falsePositives = rule.falsePositives.length > 0
|
|
126
|
+
? `\nNot a violation:\n${rule.falsePositives.map(item => `- ${item}`).join('\n')}`
|
|
127
|
+
: '';
|
|
128
|
+
const symbol = rule.confirm ? '\nProvide `symbol`: the name this finding is about.' : '';
|
|
129
|
+
return [
|
|
130
|
+
`### ${rule.id}`,
|
|
131
|
+
`Message: ${rule.message}`,
|
|
132
|
+
rule.why,
|
|
133
|
+
`Bad:\n\`\`\`\n${bad}\n\`\`\``,
|
|
134
|
+
`Good:\n\`\`\`\n${good}\n\`\`\`${falsePositives}${symbol}`,
|
|
135
|
+
].join('\n\n');
|
|
136
|
+
})
|
|
137
|
+
.join('\n\n');
|
|
138
|
+
}
|
|
139
|
+
function shortest(sources) {
|
|
140
|
+
return sources.reduce((best, source) => (source.length < best.length ? source : best), sources[0] ?? '');
|
|
141
|
+
}
|
|
142
|
+
const systemTokenCache = new Map();
|
|
143
|
+
function estimateSystemTokens(system) {
|
|
144
|
+
const cached = systemTokenCache.get(system);
|
|
145
|
+
if (cached !== undefined) {
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
148
|
+
const estimate = Math.ceil(system.length / 4);
|
|
149
|
+
systemTokenCache.set(system, estimate);
|
|
150
|
+
return estimate;
|
|
151
|
+
}
|
|
152
|
+
async function readCache(dir, key, schema) {
|
|
153
|
+
try {
|
|
154
|
+
const parsed = schema.safeParse(JSON.parse(await readFile(join(dir, `${key}.json`), 'utf-8')));
|
|
155
|
+
return parsed.success ? parsed.data : undefined;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (isMissingFile(error)) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function writeCache(dir, key, output) {
|
|
165
|
+
await mkdir(dir, { recursive: true });
|
|
166
|
+
await writeFile(join(dir, `${key}.json`), JSON.stringify(output, null, 2));
|
|
167
|
+
}
|
|
168
|
+
function isMissingFile(error) {
|
|
169
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
170
|
+
}
|
|
171
|
+
function hash(text) {
|
|
172
|
+
return createHash('sha256').update(text).digest('hex');
|
|
173
|
+
}
|
|
174
|
+
function isNonEmpty(items) {
|
|
175
|
+
return items.length > 0;
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=judge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"judge.js","sourceRoot":"","sources":["../../src/analyzers/judge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAE1C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG1E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAChC,MAAM,CAAC;IACP,sDAAsD;IACtD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,kBAAkB,CAAC;IAC7C,wEAAwE;IACxE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;IAC5D,0DAA0D;IAC1D,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACvD,uCAAuC;IACvC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACtD,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;IACpD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC1D,kDAAkD;IAClD,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;CACtD,CAAC;KACD,MAAM,EAAE,CAAC;AAIX,MAAM,CAAC,MAAM,UAAU,GAAG;IACzB,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE,WAAW;CACb,CAAC;AAIX,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE;IACzB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACrC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC3C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC3C,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC5C,OAAO,EAAE,CAAC,CAAC,KAAK,CACf,CAAC,CAAC,MAAM,CAAC;QACR,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;KACzD,CAAC,CACF;CACD,CAAC,CAAC;AAeH,MAAM,WAAW,GAAG,OAAO,CAAC;AAC5B,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAEjC,kGAAkG;AAClG,2FAA2F;AAC3F,MAAM,UAAU,WAAW,CAAC,OAAqB;IAChD,IAAI,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC;IACpD,MAAM,OAAO,GAAiB;QAC7B,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK;QAC3B,aAAa,EAAE,EAAE;QACjB,KAAK,EAAE,CAAC;QACR,WAAW,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,YAAY,EAAE,CAAC;QACf,OAAO,EAAE,EAAE;KACX,CAAC;IAEF,KAAK,UAAU,OAAO,CAAC,MAAc,EAAE,KAAsB;QAC5D,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAqB,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3F,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEhD,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1B,OAAO,EAAE,CAAC;QACX,CAAC;QAED,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;YAC/D,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC;YAE1E,OAAO,EAAE,CAAC;QACX,CAAC;QAED,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACzF,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,EAAE;YACtC,KAAK,EAAE,WAAW,CAAC,UAAU,CAAC;YAC9B,cAAc,EAAE,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM;SACtE,CAAC,CAAC;QACH,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC;QAErE,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QACnE,MAAM,GAAG,GAA4B,EAAE,CAAC;QAExC,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;YACrE,MAAM,SAAS,GAAG,KAAK,CAAC,eAAe,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;YAEvE,IAAI,SAAS,GAAG,eAAe,EAAE,CAAC;gBACjC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;gBAEvE,MAAM;YACP,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;YACvD,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9B,CAAC;QAED,OAAO,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAC/E,CAAC;IAED,KAAK,UAAU,UAAU,CAAC,KAAY,EAAE,MAAc,EAAE,MAA4C;QACnG,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACtE,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAE9D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC;YACzB,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,cAAc,CAAC,CAAC;YAE5F,OAAO,MAAM,CAAC;QACf,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC;YACjC,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;YACnD,MAAM;YACN,MAAM,EAAE,KAAK,CAAC,IAAI;YAClB,WAAW,EAAE,CAAC;YACd,UAAU,EAAE,CAAC;YACb,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe;YAC/C,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS;SACjC,CAAC,CAAC;QAEH,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC;QACnB,OAAO,CAAC,WAAW,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,CAAC;QACrD,OAAO,CAAC,YAAY,IAAI,MAAM,CAAC,KAAK,CAAC,YAAY,IAAI,CAAC,CAAC;QACvD,eAAe,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,eAAe,CAAC;QACrE,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,cAAc,CAAC,CAAC;QAEpG,MAAM,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAEvD,OAAO,MAAM,CAAC,MAAM,CAAC;IACtB,CAAC;IAED,OAAO;QACN,OAAO;QACP,OAAO,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC;KACvC,CAAC;AACH,CAAC;AAED,6FAA6F;AAC7F,SAAS,WAAW,CAAC,KAA2B;IAC/C,OAAO,KAAK;SACV,GAAG,CAAC,IAAI,CAAC,EAAE;QACX,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9D,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAChE,MAAM,cAAc,GACnB,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC;YAC7B,CAAC,CAAC,uBAAuB,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YAClF,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,qDAAqD,CAAC,CAAC,CAAC,EAAE,CAAC;QAEzF,OAAO;YACN,OAAO,IAAI,CAAC,EAAE,EAAE;YAChB,YAAY,IAAI,CAAC,OAAO,EAAE;YAC1B,IAAI,CAAC,GAAG;YACR,iBAAiB,GAAG,UAAU;YAC9B,kBAAkB,IAAI,WAAW,cAAc,GAAG,MAAM,EAAE;SAC1D,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChB,CAAC,CAAC;SACD,IAAI,CAAC,MAAM,CAAC,CAAC;AAChB,CAAC;AAED,SAAS,QAAQ,CAAC,OAA0B;IAC3C,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AAC1G,CAAC;AAED,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEnD,SAAS,oBAAoB,CAAC,MAAc;IAC3C,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAE5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,MAAM,CAAC;IACf,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC9C,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAEvC,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,SAAS,CACvB,GAAW,EACX,GAAW,EACX,MAA4C;IAE5C,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAE/F,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QAClB,CAAC;QAED,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,GAAW,EAAE,GAAW,EAAE,MAAmB;IACtE,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACpC,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;AAC7E,CAAC;AAED,SAAS,IAAI,CAAC,IAAY;IACzB,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,UAAU,CAAI,KAAmB;IACzC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;AACzB,CAAC"}
|
package/dist/change.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Lang } from './lang.js';
|
|
2
|
+
export type Range = {
|
|
3
|
+
base: string;
|
|
4
|
+
head: string;
|
|
5
|
+
};
|
|
6
|
+
export type Change = {
|
|
7
|
+
path: string;
|
|
8
|
+
lang: Lang;
|
|
9
|
+
source: string;
|
|
10
|
+
changedLines: ReadonlySet<number>;
|
|
11
|
+
scoredLines: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function readChanges(repo: string, range: Range, ignore: readonly string[]): Promise<Change[]>;
|
|
14
|
+
export declare function isIgnored(path: string, globs: readonly string[]): boolean;
|
|
15
|
+
export type DiffFile = {
|
|
16
|
+
path: string;
|
|
17
|
+
addedLines: readonly number[];
|
|
18
|
+
};
|
|
19
|
+
export declare function parseDiff(diff: string): DiffFile[];
|
package/dist/change.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { matchesGlob } from 'node:path';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { detectLang } from './lang.js';
|
|
5
|
+
import { mapConcurrent } from './shared/concurrency.js';
|
|
6
|
+
import { GitError } from './shared/errors.js';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
9
|
+
export async function readChanges(repo, range, ignore) {
|
|
10
|
+
const diff = await git(repo, [
|
|
11
|
+
'diff',
|
|
12
|
+
'--unified=0',
|
|
13
|
+
'--no-color',
|
|
14
|
+
'--diff-filter=AMR',
|
|
15
|
+
'--find-renames',
|
|
16
|
+
`${range.base}...${range.head}`,
|
|
17
|
+
]);
|
|
18
|
+
const files = parseDiff(diff).filter(file => detectLang(file.path) !== undefined && !isIgnored(file.path, ignore));
|
|
19
|
+
return mapConcurrent(files, async (file) => {
|
|
20
|
+
const source = await git(repo, ['show', `${range.head}:${file.path}`]);
|
|
21
|
+
return toChange(file, source);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function isIgnored(path, globs) {
|
|
25
|
+
return globs.some(glob => matchesGlob(path, glob));
|
|
26
|
+
}
|
|
27
|
+
const NEW_FILE_HEADER = '+++ b/';
|
|
28
|
+
const DELETED_FILE_HEADER = '+++ /dev/null';
|
|
29
|
+
const HUNK_HEADER_PATTERN = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
|
|
30
|
+
// reads a zero-context unified diff. with no context lines every hunk's `+` side is exactly the added
|
|
31
|
+
// or modified lines, so the header alone tells us which new-file lines changed.
|
|
32
|
+
export function parseDiff(diff) {
|
|
33
|
+
const files = [];
|
|
34
|
+
let current;
|
|
35
|
+
for (const line of diff.split('\n')) {
|
|
36
|
+
if (line.startsWith(DELETED_FILE_HEADER)) {
|
|
37
|
+
current = undefined;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (line.startsWith(NEW_FILE_HEADER)) {
|
|
41
|
+
current = { path: line.slice(NEW_FILE_HEADER.length).trim(), addedLines: [] };
|
|
42
|
+
files.push(current);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const hunk = HUNK_HEADER_PATTERN.exec(line);
|
|
46
|
+
if (hunk === null || current === undefined) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const start = Number(hunk[1]);
|
|
50
|
+
const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
|
|
51
|
+
for (let offset = 0; offset < count; offset += 1) {
|
|
52
|
+
current.addedLines.push(start + offset);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return files;
|
|
56
|
+
}
|
|
57
|
+
function toChange(file, source) {
|
|
58
|
+
const lang = detectLang(file.path);
|
|
59
|
+
if (lang === undefined) {
|
|
60
|
+
throw new Error(`no language for ${file.path}`);
|
|
61
|
+
}
|
|
62
|
+
const lines = source.split('\n');
|
|
63
|
+
const changedLines = new Set(file.addedLines);
|
|
64
|
+
const scoredLines = [...changedLines].filter(line => lines[line - 1]?.trim()).length;
|
|
65
|
+
return {
|
|
66
|
+
path: file.path,
|
|
67
|
+
lang,
|
|
68
|
+
source,
|
|
69
|
+
changedLines,
|
|
70
|
+
scoredLines,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function git(repo, args) {
|
|
74
|
+
try {
|
|
75
|
+
const { stdout } = await execFileAsync('git', [...args], {
|
|
76
|
+
cwd: repo,
|
|
77
|
+
encoding: 'utf-8',
|
|
78
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
79
|
+
});
|
|
80
|
+
return stdout;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const stderr = error instanceof Error && 'stderr' in error ? String(error.stderr).trim() : '';
|
|
84
|
+
throw new GitError(`git ${args[0] ?? ''} failed${stderr ? `: ${stderr}` : ''}`, { cause: error });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=change.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"change.js","sourceRoot":"","sources":["../src/change.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { createJudge } from './analyzers/judge.js';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
import { resolveModel } from './model.js';
|
|
8
|
+
import { Format, formatValues, isFormat, renderReport } from './report.js';
|
|
9
|
+
import { loadRules } from './rule.js';
|
|
10
|
+
import { scan } from './scan.js';
|
|
11
|
+
import { SlopscanError } from './shared/errors.js';
|
|
12
|
+
import { logger } from './shared/log.js';
|
|
13
|
+
import { RULES_DIR } from './shared/paths.js';
|
|
14
|
+
const HELP = `slopscan - scores a git diff for slop
|
|
15
|
+
|
|
16
|
+
usage:
|
|
17
|
+
slopscan scan [--base <ref>] [--head <ref>] [--baseline <ref>] [--format <fmt>] [--no-judge] [--config <path>]
|
|
18
|
+
slopscan rules
|
|
19
|
+
|
|
20
|
+
options:
|
|
21
|
+
--base <ref> ref to diff against (default: main)
|
|
22
|
+
--head <ref> ref to score (default: HEAD)
|
|
23
|
+
--baseline <ref> also score baseline..base and show the delta
|
|
24
|
+
--format <fmt> ${formatValues.join(', ')} (default: ${Format.TERMINAL})
|
|
25
|
+
--no-judge skip the LLM judge; syntax rules only, no API key needed
|
|
26
|
+
--config <path> path to .slopscan.yml (default: ./.slopscan.yml)
|
|
27
|
+
--rules <dir> rule directory (default: the bundled rules)
|
|
28
|
+
--json-out <path> also write the full report as json to this file
|
|
29
|
+
-h, --help show this help
|
|
30
|
+
|
|
31
|
+
exit codes: 0 under the fail threshold, 1 over it, 2 on error
|
|
32
|
+
`;
|
|
33
|
+
const CACHE_DIR = '.slopscan-cache';
|
|
34
|
+
const ExitCode = {
|
|
35
|
+
OK: 0,
|
|
36
|
+
FAILED_THRESHOLD: 1,
|
|
37
|
+
ERROR: 2,
|
|
38
|
+
};
|
|
39
|
+
const Command = {
|
|
40
|
+
SCAN: 'scan',
|
|
41
|
+
RULES: 'rules',
|
|
42
|
+
};
|
|
43
|
+
const parseOptions = {
|
|
44
|
+
options: {
|
|
45
|
+
base: { type: 'string', default: 'main' },
|
|
46
|
+
head: { type: 'string', default: 'HEAD' },
|
|
47
|
+
baseline: { type: 'string' },
|
|
48
|
+
format: { type: 'string', default: Format.TERMINAL },
|
|
49
|
+
judge: { type: 'boolean', default: true },
|
|
50
|
+
config: { type: 'string' },
|
|
51
|
+
rules: { type: 'string', default: RULES_DIR },
|
|
52
|
+
'json-out': { type: 'string' },
|
|
53
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
54
|
+
},
|
|
55
|
+
allowPositionals: true,
|
|
56
|
+
allowNegative: true,
|
|
57
|
+
strict: true,
|
|
58
|
+
};
|
|
59
|
+
async function main(argv) {
|
|
60
|
+
const { values, positionals } = parseArgs({ ...parseOptions, args: [...argv] });
|
|
61
|
+
const command = positionals[0];
|
|
62
|
+
if (values.help || command === undefined) {
|
|
63
|
+
process.stdout.write(HELP);
|
|
64
|
+
return ExitCode.OK;
|
|
65
|
+
}
|
|
66
|
+
switch (command) {
|
|
67
|
+
case Command.SCAN:
|
|
68
|
+
return runScan(values);
|
|
69
|
+
case Command.RULES:
|
|
70
|
+
return listRules(values.rules);
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`unknown command "${command}"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function runScan(values) {
|
|
76
|
+
if (!isFormat(values.format)) {
|
|
77
|
+
throw new Error(`unknown format "${values.format}"; expected one of ${formatValues.join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
const repo = process.cwd();
|
|
80
|
+
const [config, rules] = await Promise.all([loadConfig(repo, values.config), loadRules(values.rules)]);
|
|
81
|
+
const judge = values.judge ? buildJudge(repo, config) : undefined;
|
|
82
|
+
const report = await scan({
|
|
83
|
+
repo,
|
|
84
|
+
range: { base: values.base, head: values.head },
|
|
85
|
+
config,
|
|
86
|
+
rules,
|
|
87
|
+
...(judge && { judge }),
|
|
88
|
+
...(values.baseline !== undefined && { baseline: values.baseline }),
|
|
89
|
+
});
|
|
90
|
+
process.stdout.write(renderReport(report, { format: values.format, color: process.stdout.isTTY }));
|
|
91
|
+
if (values['json-out'] !== undefined) {
|
|
92
|
+
await writeFile(values['json-out'], renderReport(report, { format: Format.JSON, color: false }));
|
|
93
|
+
}
|
|
94
|
+
logger.debug({ files: report.files, findings: report.findings.length, overall: report.overall }, 'scan complete');
|
|
95
|
+
return report.overall > config.failThreshold ? ExitCode.FAILED_THRESHOLD : ExitCode.OK;
|
|
96
|
+
}
|
|
97
|
+
function buildJudge(repo, config) {
|
|
98
|
+
return createJudge({
|
|
99
|
+
config: config.judge,
|
|
100
|
+
model: resolveModel(config.judge.model),
|
|
101
|
+
cacheDir: join(repo, CACHE_DIR, 'judge'),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async function listRules(rulesDir) {
|
|
105
|
+
const rules = await loadRules(rulesDir);
|
|
106
|
+
for (const rule of rules) {
|
|
107
|
+
process.stdout.write(`${rule.id.padEnd(44)} ${rule.severity.padEnd(8)} ${rule.detect.padEnd(5)} ${rule.message}\n`);
|
|
108
|
+
}
|
|
109
|
+
return ExitCode.OK;
|
|
110
|
+
}
|
|
111
|
+
main(process.argv.slice(2))
|
|
112
|
+
.then(code => {
|
|
113
|
+
process.exitCode = code;
|
|
114
|
+
})
|
|
115
|
+
.catch((error) => {
|
|
116
|
+
// our own errors carry a readable message; anything else is a bug and deserves the stack
|
|
117
|
+
if (error instanceof SlopscanError) {
|
|
118
|
+
process.stderr.write(`error: ${error.message}\n`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
logger.error({ err: error }, 'slopscan failed');
|
|
122
|
+
}
|
|
123
|
+
process.exitCode = ExitCode.ERROR;
|
|
124
|
+
});
|
|
125
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|