@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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/SKILL.md +149 -0
  4. package/dist/analyzer.d.ts +5 -0
  5. package/dist/analyzer.js +13 -0
  6. package/dist/analyzer.js.map +1 -0
  7. package/dist/analyzers/ast.d.ts +12 -0
  8. package/dist/analyzers/ast.js +73 -0
  9. package/dist/analyzers/ast.js.map +1 -0
  10. package/dist/analyzers/judge-chunks.d.ts +13 -0
  11. package/dist/analyzers/judge-chunks.js +87 -0
  12. package/dist/analyzers/judge-chunks.js.map +1 -0
  13. package/dist/analyzers/judge-validate.d.ts +19 -0
  14. package/dist/analyzers/judge-validate.js +81 -0
  15. package/dist/analyzers/judge-validate.js.map +1 -0
  16. package/dist/analyzers/judge.d.ts +44 -0
  17. package/dist/analyzers/judge.js +177 -0
  18. package/dist/analyzers/judge.js.map +1 -0
  19. package/dist/change.d.ts +19 -0
  20. package/dist/change.js +87 -0
  21. package/dist/change.js.map +1 -0
  22. package/dist/cli.d.ts +2 -0
  23. package/dist/cli.js +125 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/config.d.ts +91 -0
  26. package/dist/config.js +61 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/confirm.d.ts +11 -0
  29. package/dist/confirm.js +100 -0
  30. package/dist/confirm.js.map +1 -0
  31. package/dist/finding.d.ts +13 -0
  32. package/dist/finding.js +18 -0
  33. package/dist/finding.js.map +1 -0
  34. package/dist/lang.d.ts +16 -0
  35. package/dist/lang.js +37 -0
  36. package/dist/lang.js.map +1 -0
  37. package/dist/model.d.ts +2 -0
  38. package/dist/model.js +34 -0
  39. package/dist/model.js.map +1 -0
  40. package/dist/report.d.ts +124 -0
  41. package/dist/report.js +112 -0
  42. package/dist/report.js.map +1 -0
  43. package/dist/rule.d.ts +66 -0
  44. package/dist/rule.js +259 -0
  45. package/dist/rule.js.map +1 -0
  46. package/dist/scan.d.ts +14 -0
  47. package/dist/scan.js +33 -0
  48. package/dist/scan.js.map +1 -0
  49. package/dist/score.d.ts +147 -0
  50. package/dist/score.js +142 -0
  51. package/dist/score.js.map +1 -0
  52. package/dist/shared/concurrency.d.ts +5 -0
  53. package/dist/shared/concurrency.js +17 -0
  54. package/dist/shared/concurrency.js.map +1 -0
  55. package/dist/shared/env.d.ts +27 -0
  56. package/dist/shared/env.js +29 -0
  57. package/dist/shared/env.js.map +1 -0
  58. package/dist/shared/errors.d.ts +18 -0
  59. package/dist/shared/errors.js +26 -0
  60. package/dist/shared/errors.js.map +1 -0
  61. package/dist/shared/log.d.ts +2 -0
  62. package/dist/shared/log.js +18 -0
  63. package/dist/shared/log.js.map +1 -0
  64. package/dist/shared/paths.d.ts +3 -0
  65. package/dist/shared/paths.js +6 -0
  66. package/dist/shared/paths.js.map +1 -0
  67. package/dist/shared/prompts.d.ts +3 -0
  68. package/dist/shared/prompts.js +34 -0
  69. package/dist/shared/prompts.js.map +1 -0
  70. package/package.json +79 -0
  71. package/prompts/judge.md +37 -0
  72. package/rules/any/futureproof/abstraction/pass-through-wrapper.md +70 -0
  73. package/rules/any/futureproof/abstraction/single-caller-helper.md +38 -0
  74. package/rules/any/futureproof/abstraction/single-impl-interface.md +83 -0
  75. package/rules/any/futureproof/exports/dead-export.md +62 -0
  76. package/rules/any/futureproof/layering/framework-type-in-domain.md +83 -0
  77. package/rules/any/futureproof/layering/logic-in-handler.md +94 -0
  78. package/rules/any/futureproof/params/boolean-positional-param.md +128 -0
  79. package/rules/any/futureproof/params/positional-config-args.md +135 -0
  80. package/rules/any/futureproof/structure/wide-function.md +138 -0
  81. package/rules/any/futureproof/testing/test-asserts-implementation.md +70 -0
  82. package/rules/any/hacky/comments/comment-restates-code.md +50 -0
  83. package/rules/any/hacky/comments/shipped-todo-comment.md +55 -0
  84. package/rules/any/hacky/concurrency/sleep-based-sync.md +95 -0
  85. package/rules/any/hacky/config/hardcoded-url.md +88 -0
  86. package/rules/any/hacky/constants/magic-number.md +94 -0
  87. package/rules/any/hacky/debugging/debug-print.md +73 -0
  88. package/rules/any/hacky/duplication/copy-paste-block.md +117 -0
  89. package/rules/any/hacky/types/stringly-typed-enum.md +86 -0
  90. package/rules/any/idiom/comments/no-section-banners.md +47 -0
  91. package/rules/go/futureproof/constants/string-enums-when-serialized.md +47 -0
  92. package/rules/go/futureproof/constants/typed-constants-for-enums.md +45 -0
  93. package/rules/go/futureproof/interfaces/compile-time-impl-assertion.md +43 -0
  94. package/rules/go/futureproof/interfaces/define-interfaces-at-consumer.md +47 -0
  95. package/rules/go/futureproof/interfaces/keep-interfaces-small.md +53 -0
  96. package/rules/go/futureproof/naming/no-utils-helpers-common-package.md +34 -0
  97. package/rules/go/futureproof/structs/zero-value-usable.md +50 -0
  98. package/rules/go/futureproof/types/generics-over-any.md +55 -0
  99. package/rules/go/hacky/concurrency/no-goroutine-without-wait.md +44 -0
  100. package/rules/go/hacky/context/never-pass-nil-context.md +64 -0
  101. package/rules/go/hacky/context/no-context-in-struct.md +44 -0
  102. package/rules/go/hacky/errors/check-close-error-on-writable.md +53 -0
  103. package/rules/go/hacky/errors/custom-error-type-errors-as.md +54 -0
  104. package/rules/go/hacky/errors/ignored-error-blank.md +84 -0
  105. package/rules/go/hacky/errors/panic-only-unrecoverable.md +77 -0
  106. package/rules/go/hacky/errors/sentinel-errors-with-errors-is.md +63 -0
  107. package/rules/go/hacky/errors/wrap-errors-with-w.md +71 -0
  108. package/rules/go/hacky/state/package-level-mutable-var.md +95 -0
  109. package/rules/go/hacky/structure/os-exit-only-in-main.md +61 -0
  110. package/rules/go/hacky/type-safety/no-map-string-any.md +49 -0
  111. package/rules/go/hacky/types/json-into-struct-not-map.md +48 -0
  112. package/rules/go/hacky/types/no-any.md +71 -0
  113. package/rules/go/hacky/types/no-map-any-any.md +35 -0
  114. package/rules/go/idiom/comments/doc-comment-on-exported.md +81 -0
  115. package/rules/go/idiom/comments/doc-comment-starts-with-name.md +36 -0
  116. package/rules/go/idiom/concurrency/mutex-over-channels-for-state.md +47 -0
  117. package/rules/go/idiom/constructors/constructor-named-new.md +37 -0
  118. package/rules/go/idiom/context/context-is-first-param.md +47 -0
  119. package/rules/go/idiom/context/io-takes-context.md +50 -0
  120. package/rules/go/idiom/errors/capitalized-error-message.md +52 -0
  121. package/rules/go/idiom/errors/check-error-immediately.md +45 -0
  122. package/rules/go/idiom/errors/defer-close-explicit-discard.md +65 -0
  123. package/rules/go/idiom/errors/error-is-last-return-value.md +53 -0
  124. package/rules/go/idiom/errors/no-log-and-return.md +45 -0
  125. package/rules/go/idiom/formatting/blank-line-before-return.md +48 -0
  126. package/rules/go/idiom/formatting/multiline-struct-literals.md +56 -0
  127. package/rules/go/idiom/imports/import-order-groups.md +42 -0
  128. package/rules/go/idiom/imports/side-effect-imports-own-group.md +51 -0
  129. package/rules/go/idiom/interfaces/accept-interfaces-return-concrete.md +39 -0
  130. package/rules/go/idiom/iterators/prefer-iter-seq.md +47 -0
  131. package/rules/go/idiom/logging/lowercase-log-messages.md +80 -0
  132. package/rules/go/idiom/logging/use-slog.md +46 -0
  133. package/rules/go/idiom/naming/acronyms-consistent-case.md +97 -0
  134. package/rules/go/idiom/naming/interface-er-suffix.md +33 -0
  135. package/rules/go/idiom/naming/no-package-name-stutter.md +37 -0
  136. package/rules/go/idiom/naming/package-name-single-lowercase-word.md +40 -0
  137. package/rules/go/idiom/receivers/consistent-receiver-kind-per-type.md +40 -0
  138. package/rules/go/idiom/receivers/no-this-receiver.md +37 -0
  139. package/rules/go/idiom/receivers/pointer-vs-value-receiver-choice.md +49 -0
  140. package/rules/go/idiom/receivers/short-receiver-names.md +43 -0
  141. package/rules/go/idiom/structs/new-expr-for-pointer-fields.md +40 -0
  142. package/rules/go/idiom/structure/main-delegates-to-run.md +54 -0
  143. package/rules/ts/futureproof/classes/composition-over-abstract-base.md +73 -0
  144. package/rules/ts/futureproof/functions/explicit-return-type-on-exports.md +50 -0
  145. package/rules/ts/futureproof/structure/colocate-zod-schemas.md +46 -0
  146. package/rules/ts/futureproof/types/exhaustive-switch-never-check.md +52 -0
  147. package/rules/ts/futureproof/types/readonly-for-immutable-data.md +46 -0
  148. package/rules/ts/futureproof/zod/enum-values-from-const-object.md +37 -0
  149. package/rules/ts/futureproof/zod/no-z-native-enum.md +36 -0
  150. package/rules/ts/futureproof/zod/schema-first-infer-type.md +46 -0
  151. package/rules/ts/hacky/async/no-floating-promises.md +52 -0
  152. package/rules/ts/hacky/config/env-read-outside-config.md +56 -0
  153. package/rules/ts/hacky/errors/catch-param-typed-unknown.md +67 -0
  154. package/rules/ts/hacky/errors/custom-error-class-instanceof.md +66 -0
  155. package/rules/ts/hacky/errors/empty-catch.md +52 -0
  156. package/rules/ts/hacky/errors/log-and-swallow.md +65 -0
  157. package/rules/ts/hacky/errors/throw-typed-error-with-context.md +64 -0
  158. package/rules/ts/hacky/operators/nullish-coalescing-over-or.md +50 -0
  159. package/rules/ts/hacky/state/exported-let.md +60 -0
  160. package/rules/ts/hacky/type-safety/no-double-assertion.md +34 -0
  161. package/rules/ts/hacky/type-safety/no-explicit-any.md +48 -0
  162. package/rules/ts/hacky/type-safety/no-non-null-assertion.md +43 -0
  163. package/rules/ts/hacky/type-safety/no-unchecked-type-assertion.md +54 -0
  164. package/rules/ts/hacky/type-safety/validate-parsed-json.md +48 -0
  165. package/rules/ts/idiom/async/no-promise-chains.md +45 -0
  166. package/rules/ts/idiom/async/parallelize-independent-awaits.md +46 -0
  167. package/rules/ts/idiom/comments/doc-comment-above-declaration.md +43 -0
  168. package/rules/ts/idiom/constants/as-const-for-literal-config.md +65 -0
  169. package/rules/ts/idiom/constants/no-enum.md +37 -0
  170. package/rules/ts/idiom/control-flow/always-brace-if.md +56 -0
  171. package/rules/ts/idiom/control-flow/guard-clauses-early-return.md +52 -0
  172. package/rules/ts/idiom/control-flow/no-nested-ternary.md +54 -0
  173. package/rules/ts/idiom/errors/error-message-lowercase.md +56 -0
  174. package/rules/ts/idiom/errors/log-at-boundary-not-every-layer.md +66 -0
  175. package/rules/ts/idiom/exports/inline-export-at-declaration.md +49 -0
  176. package/rules/ts/idiom/exports/no-default-export.md +37 -0
  177. package/rules/ts/idiom/formatting/multiline-object-literals.md +46 -0
  178. package/rules/ts/idiom/functions/arrow-for-callbacks.md +44 -0
  179. package/rules/ts/idiom/functions/function-declaration-for-top-level.md +56 -0
  180. package/rules/ts/idiom/imports/export-type-for-types.md +34 -0
  181. package/rules/ts/idiom/imports/import-order.md +66 -0
  182. package/rules/ts/idiom/imports/import-type-for-types.md +50 -0
  183. package/rules/ts/idiom/imports/side-effect-imports-last.md +42 -0
  184. package/rules/ts/idiom/logging/structured-logging-lowercase.md +45 -0
  185. package/rules/ts/idiom/naming/boolean-name-question-prefix.md +42 -0
  186. package/rules/ts/idiom/naming/descriptive-identifier-quality.md +48 -0
  187. package/rules/ts/idiom/naming/screaming-snake-module-constants.md +58 -0
  188. package/rules/ts/idiom/strings/template-literals-over-concat.md +51 -0
  189. package/rules/ts/idiom/types/interface-extends-over-intersection.md +44 -0
  190. package/rules/ts/idiom/types/null-vs-undefined-convention.md +43 -0
  191. package/rules/ts/idiom/types/prefer-type-over-interface.md +64 -0
  192. package/rules/ts/idiom/types/satisfies-over-annotation.md +43 -0
  193. package/rules/ts/idiom/types/type-predicates-for-narrowing.md +53 -0
  194. package/rules/ts/idiom/variables/const-by-default.md +41 -0
  195. package/rules/ts/idiom/variables/no-var.md +36 -0
@@ -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>;
@@ -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;
@@ -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
@@ -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"}
@@ -0,0 +1,2 @@
1
+ import type { LanguageModel } from 'ai';
2
+ export declare function resolveModel(spec: string): LanguageModel;
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"}
@@ -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