@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/score.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { findingSchema } from './finding.js';
|
|
3
|
+
import { Axis, Severity, axisValues, severityValues } from './rule.js';
|
|
4
|
+
// the four knobs. everything else about the score follows from these and the findings.
|
|
5
|
+
export const scoringSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
// base points per finding by severity
|
|
8
|
+
points: z
|
|
9
|
+
.object({
|
|
10
|
+
[Severity.INFO]: z.number().nonnegative().default(1),
|
|
11
|
+
[Severity.MINOR]: z.number().nonnegative().default(3),
|
|
12
|
+
[Severity.MAJOR]: z.number().nonnegative().default(8),
|
|
13
|
+
[Severity.CRITICAL]: z.number().nonnegative().default(20),
|
|
14
|
+
})
|
|
15
|
+
.strict()
|
|
16
|
+
.prefault({}),
|
|
17
|
+
// the k-th finding of one rule contributes points * damping^(k-1), so repeats never dominate
|
|
18
|
+
damping: z.number().min(0).max(1).default(0.5),
|
|
19
|
+
// points per 100 changed lines at which an axis reaches 63; the curve is 100 * (1 - e^(-density / scale))
|
|
20
|
+
densityScale: z.number().positive().default(12),
|
|
21
|
+
axisWeights: z
|
|
22
|
+
.object({
|
|
23
|
+
[Axis.IDIOM]: z.number().nonnegative().default(0.25),
|
|
24
|
+
[Axis.HACKY]: z.number().nonnegative().default(0.4),
|
|
25
|
+
[Axis.FUTUREPROOF]: z.number().nonnegative().default(0.35),
|
|
26
|
+
})
|
|
27
|
+
.strict()
|
|
28
|
+
.prefault({}),
|
|
29
|
+
// a floor on changed lines so a three-line diff with one finding is not scored as a disaster
|
|
30
|
+
minScoredLines: z.number().int().positive().default(50),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
export const Grade = {
|
|
34
|
+
A: 'A',
|
|
35
|
+
B: 'B',
|
|
36
|
+
C: 'C',
|
|
37
|
+
D: 'D',
|
|
38
|
+
F: 'F',
|
|
39
|
+
};
|
|
40
|
+
const gradeValues = Object.values(Grade);
|
|
41
|
+
// upper bound of each band, inclusive
|
|
42
|
+
const GRADE_BANDS = [
|
|
43
|
+
[10, Grade.A],
|
|
44
|
+
[25, Grade.B],
|
|
45
|
+
[45, Grade.C],
|
|
46
|
+
[70, Grade.D],
|
|
47
|
+
[100, Grade.F],
|
|
48
|
+
];
|
|
49
|
+
const SEVERITY_RANK = {
|
|
50
|
+
[Severity.INFO]: 0,
|
|
51
|
+
[Severity.MINOR]: 1,
|
|
52
|
+
[Severity.MAJOR]: 2,
|
|
53
|
+
[Severity.CRITICAL]: 3,
|
|
54
|
+
};
|
|
55
|
+
export const scoredFindingSchema = findingSchema.extend({
|
|
56
|
+
axis: z.enum(axisValues),
|
|
57
|
+
severity: z.enum(severityValues),
|
|
58
|
+
// what this finding added to its axis after damping
|
|
59
|
+
points: z.number().nonnegative(),
|
|
60
|
+
});
|
|
61
|
+
export const axisScoreSchema = z.object({
|
|
62
|
+
score: z.number().min(0).max(100),
|
|
63
|
+
points: z.number().nonnegative(),
|
|
64
|
+
density: z.number().nonnegative(),
|
|
65
|
+
findings: z.number().int().nonnegative(),
|
|
66
|
+
});
|
|
67
|
+
export const scoreSchema = z.object({
|
|
68
|
+
overall: z.number().int().min(0).max(100),
|
|
69
|
+
grade: z.enum(gradeValues),
|
|
70
|
+
axes: z.object({
|
|
71
|
+
[Axis.IDIOM]: axisScoreSchema,
|
|
72
|
+
[Axis.HACKY]: axisScoreSchema,
|
|
73
|
+
[Axis.FUTUREPROOF]: axisScoreSchema,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
// a score plus the findings that produced it and the size of the diff it was normalized by
|
|
77
|
+
export const scoredSchema = scoreSchema.extend({
|
|
78
|
+
files: z.number().int().nonnegative(),
|
|
79
|
+
scoredLines: z.number().int().nonnegative(),
|
|
80
|
+
// ranked by contribution, then severity, then location
|
|
81
|
+
findings: z.array(scoredFindingSchema),
|
|
82
|
+
});
|
|
83
|
+
export function score(findings, changes, rules, scoring) {
|
|
84
|
+
const rulesById = new Map(rules.map(rule => [rule.id, rule]));
|
|
85
|
+
const scoredLines = changes.reduce((sum, change) => sum + change.scoredLines, 0);
|
|
86
|
+
const normalizer = Math.max(scoredLines, scoring.minScoredLines) / 100;
|
|
87
|
+
const scored = dampen(findings, rulesById, scoring).sort(compareByContribution);
|
|
88
|
+
const axes = Object.fromEntries(axisValues.map(axis => {
|
|
89
|
+
const own = scored.filter(finding => finding.axis === axis);
|
|
90
|
+
const points = own.reduce((sum, finding) => sum + finding.points, 0);
|
|
91
|
+
const density = points / normalizer;
|
|
92
|
+
return [
|
|
93
|
+
axis,
|
|
94
|
+
{
|
|
95
|
+
score: 100 * (1 - Math.exp(-density / scoring.densityScale)),
|
|
96
|
+
points,
|
|
97
|
+
density,
|
|
98
|
+
findings: own.length,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
}));
|
|
102
|
+
const overall = Math.round(axisValues.reduce((sum, axis) => sum + scoring.axisWeights[axis] * axes[axis].score, 0));
|
|
103
|
+
return {
|
|
104
|
+
overall,
|
|
105
|
+
grade: gradeFor(overall),
|
|
106
|
+
axes,
|
|
107
|
+
files: changes.length,
|
|
108
|
+
scoredLines,
|
|
109
|
+
findings: scored,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// groups findings by rule and applies geometric damping within each group, strongest first
|
|
113
|
+
function dampen(findings, rulesById, scoring) {
|
|
114
|
+
const byRule = Map.groupBy(findings, finding => finding.ruleId);
|
|
115
|
+
return [...byRule.entries()].flatMap(([ruleId, group]) => {
|
|
116
|
+
const rule = rulesById.get(ruleId);
|
|
117
|
+
if (rule === undefined) {
|
|
118
|
+
throw new Error(`finding references unknown rule ${ruleId}`);
|
|
119
|
+
}
|
|
120
|
+
const base = scoring.points[rule.severity];
|
|
121
|
+
return group
|
|
122
|
+
.map(finding => ({ ...finding, raw: base * finding.confidence }))
|
|
123
|
+
.sort((a, b) => b.raw - a.raw)
|
|
124
|
+
.map(({ raw, ...finding }, index) => ({
|
|
125
|
+
...finding,
|
|
126
|
+
axis: rule.axis,
|
|
127
|
+
severity: rule.severity,
|
|
128
|
+
points: raw * scoring.damping ** index,
|
|
129
|
+
}));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function compareByContribution(a, b) {
|
|
133
|
+
return (b.points - a.points ||
|
|
134
|
+
SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity] ||
|
|
135
|
+
a.path.localeCompare(b.path) ||
|
|
136
|
+
a.line - b.line);
|
|
137
|
+
}
|
|
138
|
+
export function gradeFor(overall) {
|
|
139
|
+
const band = GRADE_BANDS.find(([upper]) => overall <= upper);
|
|
140
|
+
return band?.[1] ?? Grade.F;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.js","sourceRoot":"","sources":["../src/score.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import pLimit from 'p-limit';
|
|
2
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
3
|
+
// maps items concurrently with a sliding window limit.
|
|
4
|
+
// fails fast: one rejection rejects the entire batch.
|
|
5
|
+
export async function mapConcurrent(items, mapper, options = {}) {
|
|
6
|
+
const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
7
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
8
|
+
throw new Error('concurrency must be a positive integer');
|
|
9
|
+
}
|
|
10
|
+
const limit = pLimit(concurrency);
|
|
11
|
+
const tasks = items.map(item => limit(() => {
|
|
12
|
+
options.signal?.throwIfAborted();
|
|
13
|
+
return mapper(item);
|
|
14
|
+
}));
|
|
15
|
+
return Promise.all(tasks);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=concurrency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../../src/shared/concurrency.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAO7B,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,uDAAuD;AACvD,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,KAAmB,EACnB,MAA+B,EAC/B,UAA8B,EAAE;IAEhC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,mBAAmB,CAAC;IAE/D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IAElC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAC9B,KAAK,CAAC,GAAG,EAAE;QACV,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;QAEjC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC,CAAC,CACF,CAAC;IAEF,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const LogLevel: {
|
|
3
|
+
readonly DEBUG: "debug";
|
|
4
|
+
readonly INFO: "info";
|
|
5
|
+
readonly WARN: "warn";
|
|
6
|
+
readonly ERROR: "error";
|
|
7
|
+
readonly SILENT: "silent";
|
|
8
|
+
};
|
|
9
|
+
export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
|
|
10
|
+
declare const envSchema: z.ZodObject<{
|
|
11
|
+
OPENAI_API_KEY: z.ZodOptional<z.ZodString>;
|
|
12
|
+
OPENROUTER_API_KEY: z.ZodOptional<z.ZodString>;
|
|
13
|
+
LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
|
|
14
|
+
error: "error";
|
|
15
|
+
info: "info";
|
|
16
|
+
debug: "debug";
|
|
17
|
+
warn: "warn";
|
|
18
|
+
silent: "silent";
|
|
19
|
+
}>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type Env = z.infer<typeof envSchema>;
|
|
22
|
+
export declare const env: {
|
|
23
|
+
LOG_LEVEL: "error" | "info" | "debug" | "warn" | "silent";
|
|
24
|
+
OPENAI_API_KEY?: string | undefined;
|
|
25
|
+
OPENROUTER_API_KEY?: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ConfigError } from './errors.js';
|
|
3
|
+
export const LogLevel = {
|
|
4
|
+
DEBUG: 'debug',
|
|
5
|
+
INFO: 'info',
|
|
6
|
+
WARN: 'warn',
|
|
7
|
+
ERROR: 'error',
|
|
8
|
+
SILENT: 'silent',
|
|
9
|
+
};
|
|
10
|
+
const logLevelValues = Object.values(LogLevel);
|
|
11
|
+
// provider keys are optional here: a scan without the judge needs none,
|
|
12
|
+
// and the judge asks for the one it needs with a clear error at use time.
|
|
13
|
+
const envSchema = z.object({
|
|
14
|
+
OPENAI_API_KEY: z.string().min(1).optional(),
|
|
15
|
+
OPENROUTER_API_KEY: z.string().min(1).optional(),
|
|
16
|
+
LOG_LEVEL: z.enum(logLevelValues).default(LogLevel.INFO),
|
|
17
|
+
});
|
|
18
|
+
function parseEnv() {
|
|
19
|
+
// a copied .env.example leaves keys blank; blank means unset, not "the empty string"
|
|
20
|
+
const present = Object.fromEntries(Object.entries(process.env).filter(([, value]) => value));
|
|
21
|
+
const result = envSchema.safeParse(present);
|
|
22
|
+
if (result.success) {
|
|
23
|
+
return result.data;
|
|
24
|
+
}
|
|
25
|
+
const issues = result.error.issues.map(issue => ` - ${issue.path.join('.')}: ${issue.message}`);
|
|
26
|
+
throw new ConfigError(['invalid environment variables:', ...issues].join('\n'));
|
|
27
|
+
}
|
|
28
|
+
export const env = parseEnv();
|
|
29
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/shared/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,CAAC,MAAM,QAAQ,GAAG;IACvB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;CACP,CAAC;AAIX,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAA8B,CAAC;AAE5E,wEAAwE;AACxE,0EAA0E;AAC1E,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1B,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC5C,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAChD,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;CACxD,CAAC,CAAC;AAIH,SAAS,QAAQ;IAChB,qFAAqF;IACrF,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7F,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAE5C,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,MAAM,CAAC,IAAI,CAAC;IACpB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAEhG,MAAM,IAAI,WAAW,CAAC,CAAC,gCAAgC,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG,QAAQ,EAAE,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type ErrorOptions = {
|
|
2
|
+
cause?: unknown;
|
|
3
|
+
};
|
|
4
|
+
export declare class SlopscanError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
constructor(message: string, code: string, options?: ErrorOptions);
|
|
7
|
+
}
|
|
8
|
+
export declare class ConfigError extends SlopscanError {
|
|
9
|
+
constructor(message: string, options?: ErrorOptions);
|
|
10
|
+
}
|
|
11
|
+
export declare class RuleError extends SlopscanError {
|
|
12
|
+
readonly rulePath: string;
|
|
13
|
+
constructor(rulePath: string, message: string, options?: ErrorOptions);
|
|
14
|
+
}
|
|
15
|
+
export declare class GitError extends SlopscanError {
|
|
16
|
+
constructor(message: string, options?: ErrorOptions);
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class SlopscanError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(message, code, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = this.constructor.name;
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class ConfigError extends SlopscanError {
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message, 'CONFIG', options);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class RuleError extends SlopscanError {
|
|
15
|
+
rulePath;
|
|
16
|
+
constructor(rulePath, message, options) {
|
|
17
|
+
super(`${rulePath}: ${message}`, 'RULE', options);
|
|
18
|
+
this.rulePath = rulePath;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class GitError extends SlopscanError {
|
|
22
|
+
constructor(message, options) {
|
|
23
|
+
super(message, 'GIT', options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/shared/errors.ts"],"names":[],"mappings":"AAIA,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC9B,IAAI,CAAS;IAEtB,YAAY,OAAe,EAAE,IAAY,EAAE,OAAsB;QAChE,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD;AAED,MAAM,OAAO,WAAY,SAAQ,aAAa;IAC7C,YAAY,OAAe,EAAE,OAAsB;QAClD,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC;CACD;AAED,MAAM,OAAO,SAAU,SAAQ,aAAa;IAClC,QAAQ,CAAS;IAE1B,YAAY,QAAgB,EAAE,OAAe,EAAE,OAAsB;QACpE,KAAK,CAAC,GAAG,QAAQ,KAAK,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC1B,CAAC;CACD;AAED,MAAM,OAAO,QAAS,SAAQ,aAAa;IAC1C,YAAY,OAAe,EAAE,OAAsB;QAClD,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IAChC,CAAC;CACD"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { env } from './env.js';
|
|
3
|
+
const STDERR_FD = 2;
|
|
4
|
+
// logs go to stderr so stdout stays clean for machine-readable report output.
|
|
5
|
+
// pretty-print only when a human is watching.
|
|
6
|
+
export const logger = process.stderr.isTTY
|
|
7
|
+
? pino({
|
|
8
|
+
level: env.LOG_LEVEL,
|
|
9
|
+
transport: {
|
|
10
|
+
target: 'pino-pretty',
|
|
11
|
+
options: {
|
|
12
|
+
destination: STDERR_FD,
|
|
13
|
+
translateTime: 'SYS:standard',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
: pino({ level: env.LOG_LEVEL }, pino.destination(STDERR_FD));
|
|
18
|
+
//# sourceMappingURL=log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/shared/log.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,SAAS,GAAG,CAAC,CAAC;AAEpB,8EAA8E;AAC9E,8CAA8C;AAC9C,MAAM,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK;IACzC,CAAC,CAAC,IAAI,CAAC;QACL,KAAK,EAAE,GAAG,CAAC,SAAS;QACpB,SAAS,EAAE;YACV,MAAM,EAAE,aAAa;YACrB,OAAO,EAAE;gBACR,WAAW,EAAE,SAAS;gBACtB,aAAa,EAAE,cAAc;aAC7B;SACD;KACD,CAAC;IACH,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
// src/shared and dist/shared sit at the same depth, so the package root is two levels up either way.
|
|
3
|
+
export const PACKAGE_ROOT = resolve(import.meta.dirname, '..', '..');
|
|
4
|
+
export const RULES_DIR = resolve(PACKAGE_ROOT, 'rules');
|
|
5
|
+
export const PROMPTS_DIR = resolve(PACKAGE_ROOT, 'prompts');
|
|
6
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/shared/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,qGAAqG;AACrG,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAErE,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { PROMPTS_DIR } from './paths.js';
|
|
4
|
+
const DIRECTIVE_PATTERN = /\{\{([A-Z][A-Z0-9_]*)\}\}/g;
|
|
5
|
+
const templateCache = new Map();
|
|
6
|
+
function readTemplate(name) {
|
|
7
|
+
const cached = templateCache.get(name);
|
|
8
|
+
if (cached !== undefined) {
|
|
9
|
+
return cached;
|
|
10
|
+
}
|
|
11
|
+
const content = readFileSync(join(PROMPTS_DIR, `${name}.md`), 'utf-8');
|
|
12
|
+
templateCache.set(name, content);
|
|
13
|
+
return content;
|
|
14
|
+
}
|
|
15
|
+
// loads prompts/{name}.md and resolves {{DIRECTIVE}} placeholders.
|
|
16
|
+
// throws on unresolved directives and on unused variables so drift is caught at the call site.
|
|
17
|
+
export function loadPrompt(name, vars = {}) {
|
|
18
|
+
const template = readTemplate(name);
|
|
19
|
+
const usedVars = new Set();
|
|
20
|
+
const content = template.replace(DIRECTIVE_PATTERN, (_match, key) => {
|
|
21
|
+
const value = vars[key];
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
throw new Error(`unresolved directive {{${key}}} in prompt "${name}"`);
|
|
24
|
+
}
|
|
25
|
+
usedVars.add(key);
|
|
26
|
+
return value;
|
|
27
|
+
});
|
|
28
|
+
const unusedVars = Object.keys(vars).filter(key => !usedVars.has(key));
|
|
29
|
+
if (unusedVars.length > 0) {
|
|
30
|
+
throw new Error(`unused vars passed to prompt "${name}": ${unusedVars.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
return content;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/shared/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,iBAAiB,GAAG,4BAA4B,CAAC;AAIvD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEhD,SAAS,YAAY,CAAC,IAAY;IACjC,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAEvC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,MAAM,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,IAAI,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;IACvE,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,mEAAmE;AACnE,+FAA+F;AAC/F,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAmB,EAAE;IAC7D,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAEnC,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,GAAW,EAAE,EAAE;QAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QAExB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,0BAA0B,GAAG,iBAAiB,IAAI,GAAG,CAAC,CAAC;QACxE,CAAC;QAED,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAElB,OAAO,KAAK,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAEvE,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrF,CAAC;IAED,OAAO,OAAO,CAAC;AAChB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aarock1234/slopscan",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "scores a git diff for slop: hacky, non-idiomatic, non-future-proof code",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"code-review",
|
|
7
|
+
"lint",
|
|
8
|
+
"typescript",
|
|
9
|
+
"go",
|
|
10
|
+
"ast-grep",
|
|
11
|
+
"llm",
|
|
12
|
+
"github-action"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/aarock1234/slopscan.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/aarock1234/slopscan#readme",
|
|
20
|
+
"bugs": "https://github.com/aarock1234/slopscan/issues",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24"
|
|
24
|
+
},
|
|
25
|
+
"packageManager": "pnpm@12.4.1",
|
|
26
|
+
"bin": {
|
|
27
|
+
"slopscan": "dist/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"rules",
|
|
32
|
+
"prompts",
|
|
33
|
+
"SKILL.md"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "node --env-file-if-exists=.env --import tsx src/cli.ts",
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"lint": "eslint .",
|
|
42
|
+
"lint:fix": "eslint . --fix",
|
|
43
|
+
"format": "prettier --write .",
|
|
44
|
+
"format:check": "prettier --check .",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"check": "concurrently \"pnpm typecheck\" \"pnpm lint\" \"pnpm format:check\" \"pnpm test\"",
|
|
49
|
+
"script": "node --env-file-if-exists=.env --import tsx",
|
|
50
|
+
"clean": "rm -rf dist coverage *.tsbuildinfo",
|
|
51
|
+
"prepublishOnly": "pnpm check && pnpm build"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@ai-sdk/openai": "^4.0.71",
|
|
55
|
+
"@ast-grep/lang-go": "^0.0.6",
|
|
56
|
+
"@ast-grep/napi": "^0.45.3",
|
|
57
|
+
"@openrouter/ai-sdk-provider": "^3.1.0",
|
|
58
|
+
"ai": "^7.0.107",
|
|
59
|
+
"gray-matter": "^4.0.3",
|
|
60
|
+
"p-limit": "^7.3.3",
|
|
61
|
+
"picocolors": "^1.1.1",
|
|
62
|
+
"pino": "^10.3.1",
|
|
63
|
+
"pino-pretty": "^13.1.3",
|
|
64
|
+
"yaml": "^2.9.1",
|
|
65
|
+
"zod": "^4.6.5"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@eslint/js": "^10.0.1",
|
|
69
|
+
"@types/node": "^26.6.2",
|
|
70
|
+
"concurrently": "^10.0.5",
|
|
71
|
+
"eslint": "^10.11.0",
|
|
72
|
+
"jiti": "^2.7.0",
|
|
73
|
+
"prettier": "^3.9.8",
|
|
74
|
+
"tsx": "^4.23.13",
|
|
75
|
+
"typescript": "^6.0.3",
|
|
76
|
+
"typescript-eslint": "^8.70.0",
|
|
77
|
+
"vitest": "^5.0.1"
|
|
78
|
+
}
|
|
79
|
+
}
|
package/prompts/judge.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Slop judge
|
|
2
|
+
|
|
3
|
+
You review one changed source file against a fixed list of rules and report violations. The rules cover code
|
|
4
|
+
that is hacky, non-idiomatic, or hard to change later. You are not reviewing correctness, performance, or taste
|
|
5
|
+
outside the rules.
|
|
6
|
+
|
|
7
|
+
## What you receive
|
|
8
|
+
|
|
9
|
+
The file's path and language, then the changed regions of the file at its current version. Each line carries a
|
|
10
|
+
gutter: a line number, and `+` when this change added or modified that line. Unmarked lines are context that
|
|
11
|
+
someone else wrote earlier.
|
|
12
|
+
|
|
13
|
+
## What you report
|
|
14
|
+
|
|
15
|
+
A list of findings. Each finding names exactly one rule by id, the line number where it starts, a verbatim quote
|
|
16
|
+
of the offending code, a one-line message, and a confidence from 0 to 1.
|
|
17
|
+
|
|
18
|
+
Hard constraints:
|
|
19
|
+
|
|
20
|
+
- Report only on `+` lines. A problem on an unmarked line is not this change's fault.
|
|
21
|
+
- Use only the rule ids listed below. Never invent a rule.
|
|
22
|
+
- `quote` is copied character for character from the file, starting on the reported line. It may span a few
|
|
23
|
+
lines. If you cannot quote it, do not report it.
|
|
24
|
+
- `message` is lowercase, specific to this occurrence, and says what to do instead. No filler.
|
|
25
|
+
- `confidence` is your honest probability that a careful reviewer would agree. Under 0.6 means you should not
|
|
26
|
+
report it at all.
|
|
27
|
+
- Some rules ask for a `symbol`: the name of the function, type, or variable the finding is about. Provide it
|
|
28
|
+
exactly as written in the code. Set it to null for rules that do not ask.
|
|
29
|
+
- Prefer few precise findings over many vague ones. A clean file yields an empty list.
|
|
30
|
+
|
|
31
|
+
## Rules already checked mechanically
|
|
32
|
+
|
|
33
|
+
These rule ids are enforced by a syntax matcher and are not yours to report: {{HANDLED_BY_AST}}
|
|
34
|
+
|
|
35
|
+
## Rules to judge
|
|
36
|
+
|
|
37
|
+
{{RULES}}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
confirm: callCount
|
|
5
|
+
falsePositives:
|
|
6
|
+
- a method that exists to satisfy an interface or adapt one signature to another
|
|
7
|
+
- a wrapper that is the package's public surface over an internal or third-party dependency, so callers do not import the dependency
|
|
8
|
+
- a wrapper that adds something, such as a default argument, a conversion, error context, logging, or caching
|
|
9
|
+
- functions with several callers, where the shared name is the point
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
A function whose whole body forwards its arguments to another call adds a name, a file jump, and a signature to keep in sync, without adding a decision. The reader now has to open it to learn it does nothing, and the next change has to update two signatures. Call the target directly, or make the wrapper earn its name by doing something the callers should not repeat.
|
|
15
|
+
|
|
16
|
+
## Message
|
|
17
|
+
|
|
18
|
+
pass-through wrapper forwards its arguments unchanged; call the target directly
|
|
19
|
+
|
|
20
|
+
## Bad
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// BAD: forwards every argument and adds nothing
|
|
24
|
+
async function findUser(id: string): Promise<User | undefined> {
|
|
25
|
+
return userRepo.findById(id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function profile(id: string): Promise<Profile> {
|
|
29
|
+
const user = await findUser(id);
|
|
30
|
+
|
|
31
|
+
return toProfile(user);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```go
|
|
36
|
+
// BAD: forwards every argument and adds nothing
|
|
37
|
+
func (s *Service) findUser(ctx context.Context, id string) (User, error) {
|
|
38
|
+
return s.repo.FindByID(ctx, id)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func (s *Service) Profile(ctx context.Context, id string) (Profile, error) {
|
|
42
|
+
user, err := s.findUser(ctx, id)
|
|
43
|
+
if err != nil {
|
|
44
|
+
return Profile{}, fmt.Errorf("finding user: %w", err)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return toProfile(user), nil
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Good
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
export async function profile(id: string): Promise<Profile> {
|
|
55
|
+
const user = await userRepo.findById(id);
|
|
56
|
+
|
|
57
|
+
return toProfile(user);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```go
|
|
62
|
+
func (s *Service) Profile(ctx context.Context, id string) (Profile, error) {
|
|
63
|
+
user, err := s.repo.FindByID(ctx, id)
|
|
64
|
+
if err != nil {
|
|
65
|
+
return Profile{}, fmt.Errorf("finding user: %w", err)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return toProfile(user), nil
|
|
69
|
+
}
|
|
70
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
confirm: callCount
|
|
5
|
+
falsePositives:
|
|
6
|
+
- a function extracted so it can be unit tested in isolation
|
|
7
|
+
- a function that names a non-obvious step even though it is called once
|
|
8
|
+
- exported functions, which may have callers outside this repository
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Pulling three lines into a helper that is called from exactly one place adds a name to learn and a jump to follow without removing any duplication. The reader now holds two locations in their head instead of one. Extract when there is a second caller or when the name carries real meaning; otherwise keep the code where it is used.
|
|
14
|
+
|
|
15
|
+
## Message
|
|
16
|
+
|
|
17
|
+
single-use helper adds indirection without removing duplication
|
|
18
|
+
|
|
19
|
+
## Bad
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// BAD: a pass-through with one caller
|
|
23
|
+
function getUserName(user: User): string {
|
|
24
|
+
return user.name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function greet(user: User): string {
|
|
28
|
+
return `hello ${getUserName(user)}`;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Good
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export function greet(user: User): string {
|
|
36
|
+
return `hello ${user.name}`;
|
|
37
|
+
}
|
|
38
|
+
```
|