@heyramzi/lint 0.0.0-stage → 1.32.2

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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +277 -2
  3. package/bin/heyramzi-comment-length.js +99 -0
  4. package/bin/heyramzi-commit-msg.js +31 -0
  5. package/bin/heyramzi-file-tokens.js +66 -0
  6. package/bin/heyramzi-inline-path.js +79 -0
  7. package/bin/heyramzi-jev-code.js +107 -0
  8. package/bin/heyramzi-push-diff.js +93 -0
  9. package/bin/heyramzi-repo-rules.js +70 -0
  10. package/bin/heyramzi-slop.js +314 -0
  11. package/bin/heyramzi-svelte-markup.js +50 -0
  12. package/bin/heyramzi-ui-rules.js +56 -0
  13. package/comment-length.js +368 -0
  14. package/commit-msg.js +57 -0
  15. package/config/oxlintrc.json +87 -0
  16. package/data/invisibles.js +218 -0
  17. package/data/jev-bench.js +115 -0
  18. package/data/jev-code-bench.js +313 -0
  19. package/data/slop-words.d.ts +64 -0
  20. package/data/slop-words.js +2681 -0
  21. package/file-tokens.js +122 -0
  22. package/gate-files.js +14 -0
  23. package/index.js +116 -0
  24. package/inline-path.js +90 -0
  25. package/jev-code.js +194 -0
  26. package/jev.js +341 -0
  27. package/package.json +80 -4
  28. package/processors/markdown.js +22 -0
  29. package/push-diff.js +55 -0
  30. package/register.d.ts +43 -0
  31. package/register.js +255 -0
  32. package/repo-rules/packages.js +164 -0
  33. package/repo-rules/paths.js +101 -0
  34. package/repo-rules/toolchain.js +172 -0
  35. package/repo-rules/tree.js +51 -0
  36. package/repo-rules/workflows.js +116 -0
  37. package/repo-rules.js +54 -0
  38. package/rules/convex-ast.js +66 -0
  39. package/rules/convex-index-name.js +40 -0
  40. package/rules/convex-internal-only.js +59 -0
  41. package/rules/convex-no-date-now-in-query.js +48 -0
  42. package/rules/convex-no-deprecated-crons.js +40 -0
  43. package/rules/convex-no-filter.js +41 -0
  44. package/rules/convex-no-get-metadata.js +26 -0
  45. package/rules/convex-no-v-any.js +24 -0
  46. package/rules/convex-react-client-module-scope.js +37 -0
  47. package/rules/convex-require-args.js +39 -0
  48. package/rules/convex-require-returns.js +31 -0
  49. package/rules/convex-runtime-imports.js +48 -0
  50. package/rules/max-function-lines.js +105 -0
  51. package/rules/max-object-methods.js +86 -0
  52. package/rules/no-ai-words.js +78 -0
  53. package/rules/no-app-stores.js +25 -0
  54. package/rules/no-chained-type-assertions.js +91 -0
  55. package/rules/no-class.js +47 -0
  56. package/rules/no-conditional-empty-object-spread.js +64 -0
  57. package/rules/no-deep-phosphor-imports.js +29 -0
  58. package/rules/no-default-export.js +70 -0
  59. package/rules/no-em-dash.js +194 -0
  60. package/rules/no-emoji.js +56 -0
  61. package/rules/no-hardcoded-models.js +63 -0
  62. package/rules/no-inline-path.js +46 -0
  63. package/rules/no-inline-svg.js +67 -0
  64. package/rules/no-known-value-widening.js +289 -0
  65. package/rules/no-legacy-compat.js +35 -0
  66. package/rules/no-overkill-fallback.js +77 -0
  67. package/rules/no-raw-try-catch.js +26 -0
  68. package/rules/no-redundant-prop-fallback.js +153 -0
  69. package/rules/no-relative-imports.js +213 -0
  70. package/rules/no-relative-type-imports.js +40 -0
  71. package/rules/no-rune-outside-svelte-file.js +56 -0
  72. package/rules/no-rune-shadowing-binding.js +85 -0
  73. package/rules/no-skipped-or-focused-tests.js +74 -0
  74. package/rules/no-snake-case-props.js +45 -0
  75. package/rules/no-svelte4-syntax.js +33 -0
  76. package/rules/no-vitest-import-in-vite-plus.js +64 -0
  77. package/rules/props-interface.js +44 -0
  78. package/rules/redirect-external-needs-option.js +45 -0
  79. package/rules/require-safety-comment-for-type-assertion.js +110 -0
  80. package/rules/svelte-section-order.js +115 -0
  81. package/rules/test-file.js +8 -0
  82. package/scripts/count-hits.mjs +213 -0
  83. package/scripts/harvest-voice.mjs +161 -0
  84. package/scripts/jev-bench.mjs +41 -0
  85. package/scripts/jev-code-bench.mjs +61 -0
  86. package/scripts/measure-register.mjs +61 -0
  87. package/slop.d.ts +53 -0
  88. package/slop.js +644 -0
  89. package/svelte-markup.js +107 -0
  90. package/svelte-template.js +258 -0
  91. package/ui-rules.js +324 -0
package/file-tokens.js ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The file-token gate: no staged file an agent writes may run past 5,000 tokens.
3
+ * Pure here, git and process in `bin/heyramzi-file-tokens.js`.
4
+ */
5
+
6
+ export const MAX_TOKENS = 5000;
7
+ export const BYTES_PER_TOKEN = 4;
8
+
9
+ /** Generated, vendored or projected trees: nobody hand-writes these, so nobody splits them. */
10
+ export const DEFAULT_IGNORED = [
11
+ "node_modules",
12
+ "dist",
13
+ "build",
14
+ "out",
15
+ "coverage",
16
+ "vendor",
17
+ ".upstream",
18
+ "generated",
19
+ "_generated",
20
+ "__generated__",
21
+ "__snapshots__",
22
+ "paraglide",
23
+ ".svelte-kit",
24
+ ".wrangler",
25
+ ".vercel",
26
+ ".next",
27
+ ".agents",
28
+ ".claude",
29
+ ".codex",
30
+ ".gemini",
31
+ ".opencode",
32
+ "raw",
33
+ // Boards are one document per subject by the playground skill's rule, and `playground check` gates them.
34
+ "playground",
35
+ ];
36
+
37
+ /** Projections that live under a tracked folder agents also write in. */
38
+ const IGNORED_PREFIXES = [".github/skills/"];
39
+
40
+ /** Data, transcripts and tool-managed files: nobody splits a dataset, a call recording or an Xcode project by hand. */
41
+ const DATA_EXTENSIONS = /\.(json|jsonl|ndjson|csv|tsv|txt|srt|vtt|pdf|xcstrings|strings|pbxproj|excalidraw|tldr|geojson)$/i;
42
+
43
+ const LOCKFILES = new Set([
44
+ "pnpm-lock.yaml",
45
+ "package-lock.json",
46
+ "npm-shrinkwrap.json",
47
+ "yarn.lock",
48
+ "bun.lock",
49
+ "bun.lockb",
50
+ "deno.lock",
51
+ "Cargo.lock",
52
+ "Gemfile.lock",
53
+ "poetry.lock",
54
+ "uv.lock",
55
+ "Pipfile.lock",
56
+ "composer.lock",
57
+ "Podfile.lock",
58
+ "Package.resolved",
59
+ "flake.lock",
60
+ "go.sum",
61
+ ]);
62
+
63
+ /** Release tooling writes these, and a minified bundle, source map or snapshot is machine output. */
64
+ const GENERATED_NAMES = [/^CHANGELOG\.md$/, /\.generated\.\w+$/,/\.min\.(js|css)$/, /\.map$/, /\.snap$/, /\.tsbuildinfo$/, /\.svg$/];
65
+
66
+ /** A header any codegen tool writes. Read from the first 2 KB only. */
67
+ const GENERATED_HEADER = /@generated|auto-?generated|do not edit|generated by /i;
68
+
69
+ export function tokens(bytes) {
70
+ return Math.ceil(bytes / BYTES_PER_TOKEN);
71
+ }
72
+
73
+ /** True when the path itself says a machine wrote the file. */
74
+ export function ignoredPath(file) {
75
+ const parts = file.split("/");
76
+ const name = parts.at(-1);
77
+ if (LOCKFILES.has(name) || DATA_EXTENSIONS.test(name)) return true;
78
+ if (GENERATED_NAMES.some((pattern) => pattern.test(name))) return true;
79
+ if (IGNORED_PREFIXES.some((prefix) => file.startsWith(prefix))) return true;
80
+ return parts.slice(0, -1).some((part) => DEFAULT_IGNORED.includes(part.toLowerCase().replace(/^\W+\s*/u, "")));
81
+ }
82
+
83
+ /** True when the content says a machine wrote it: a NUL byte (binary, as git decides) or a codegen header. */
84
+ export function generatedContent(head) {
85
+ if (head.includes(0)) return true;
86
+ return GENERATED_HEADER.test(head.subarray(0, 2048).toString("utf8"));
87
+ }
88
+
89
+ /** Files over the cap, biggest first. `sizes` maps path to staged bytes. */
90
+ export function check(sizes, max = MAX_TOKENS) {
91
+ return Object.entries(sizes)
92
+ .map(([file, bytes]) => ({ file, bytes, tokens: tokens(bytes) }))
93
+ .filter((entry) => entry.tokens > max)
94
+ .sort((left, right) => right.tokens - left.tokens);
95
+ }
96
+
97
+ /** The refusal. It tells the agent how to split, because "too big" alone gets a file squashed, not split. */
98
+ export function report(failures, max = MAX_TOKENS) {
99
+ const rows = failures.map(
100
+ ({ file, bytes, tokens: count }) =>
101
+ ` ${file} ${count.toLocaleString("en-US")} tokens (${bytes.toLocaleString("en-US")} bytes), ${Math.round((count / max) * 100)}% of the cap`,
102
+ );
103
+ return `file-tokens: ${failures.length} staged file(s) over ${max.toLocaleString("en-US")} tokens (tokens = bytes / ${BYTES_PER_TOKEN}):
104
+ ${rows.join("\n")}
105
+
106
+ Split each file before you commit. Don't get under the cap by deleting comments, squashing lines or shortening names: that keeps one file doing too many jobs.
107
+
108
+ How to split:
109
+ 1. List what the file does. Read its exports and top-level blocks and group them by job: types, pure helpers, I/O (fetch, db, fs), state, UI sections, CLI commands, constants and data.
110
+ 2. Give each job its own file, named for that job. foo.ts becomes a foo/ folder with foo/types.ts, foo/parse.ts, foo/client.ts. Aim for each new file under half the cap.
111
+ 3. Move code, don't rewrite it. Cut each group into its new file and import what it needs from its siblings.
112
+ 4. Fix every importer. Search the repo for the old path and point each import at the new file. Don't leave a re-export file at the old path.
113
+ 5. By file type:
114
+ - Svelte component: move each visual section into a child component and the state and logic into a .svelte.ts module beside it.
115
+ - Test file: one test file per module under test, next to that module.
116
+ - Markdown (SKILL.md, a reference, docs): keep the overview and move each detailed topic into its own references/<topic>.md, linked from the overview in one line.
117
+ - Data or fixtures inside code: move them to a .json file and import it.
118
+ - Swift: one type per file, extensions in their own Type+Feature.swift.
119
+ 6. Run the repo's typecheck and tests, then stage the new files and the deletions (git add -A <folder>) and commit again.
120
+
121
+ Only a file no person or agent writes by hand (codegen, a lockfile, vendored code) is exempt. Mark real codegen with an @generated header or a linguist-generated line in .gitattributes. Never mark a file you wrote.`;
122
+ }
package/gate-files.js ADDED
@@ -0,0 +1,14 @@
1
+ import { existsSync, lstatSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /** The files a gate judges: no symlinks, and only the pushed ones when the hook sets PUSH_FILES. */
5
+ export function gateFiles(root, files) {
6
+ const pushed =
7
+ process.env.PUSH_FILES === undefined ? null : new Set(process.env.PUSH_FILES.split("\n"));
8
+ return files.filter((file) => {
9
+ if (pushed && !pushed.has(file)) return false;
10
+ // A symlink points at a file the gate already reads at its real path.
11
+ const full = path.join(root, file);
12
+ return existsSync(full) && !lstatSync(full).isSymbolicLink();
13
+ });
14
+ }
package/index.js ADDED
@@ -0,0 +1,116 @@
1
+ // Code quality rules (from @upsys/config)
2
+ import noRelativeImports from "#lint/rules/no-relative-imports.js";
3
+ import noRelativeTypeImports from "#lint/rules/no-relative-type-imports.js";
4
+ import noDeepPhosphorImports from "#lint/rules/no-deep-phosphor-imports.js";
5
+ import noClass from "#lint/rules/no-class.js";
6
+ import noDefaultExport from "#lint/rules/no-default-export.js";
7
+ import noOverkillFallback from "#lint/rules/no-overkill-fallback.js";
8
+ import noRedundantPropFallback from "#lint/rules/no-redundant-prop-fallback.js";
9
+ import maxObjectMethods from "#lint/rules/max-object-methods.js";
10
+ import maxFunctionLines from "#lint/rules/max-function-lines.js";
11
+ import noSnakeCaseProps from "#lint/rules/no-snake-case-props.js";
12
+ import noHardcodedModels from "#lint/rules/no-hardcoded-models.js";
13
+ import noLegacyCompat from "#lint/rules/no-legacy-compat.js";
14
+
15
+ // Convex rules (the checks @convex-dev/eslint-plugin never ran here, plus the house rules)
16
+ import convexIndexName from "#lint/rules/convex-index-name.js";
17
+ import convexInternalOnly from "#lint/rules/convex-internal-only.js";
18
+ import convexNoDateNowInQuery from "#lint/rules/convex-no-date-now-in-query.js";
19
+ import convexNoDeprecatedCrons from "#lint/rules/convex-no-deprecated-crons.js";
20
+ import convexNoFilter from "#lint/rules/convex-no-filter.js";
21
+ import convexNoGetMetadata from "#lint/rules/convex-no-get-metadata.js";
22
+ import convexNoVAny from "#lint/rules/convex-no-v-any.js";
23
+ import convexReactClientModuleScope from "#lint/rules/convex-react-client-module-scope.js";
24
+ import convexRequireArgs from "#lint/rules/convex-require-args.js";
25
+ import convexRequireReturns from "#lint/rules/convex-require-returns.js";
26
+ import convexRuntimeImports from "#lint/rules/convex-runtime-imports.js";
27
+
28
+ // Type-evidence rules (ported from dmmulroy/anti-slop)
29
+ import noChainedTypeAssertions from "#lint/rules/no-chained-type-assertions.js";
30
+ import noConditionalEmptyObjectSpread from "#lint/rules/no-conditional-empty-object-spread.js";
31
+ import noKnownValueWidening from "#lint/rules/no-known-value-widening.js";
32
+ import requireSafetyCommentForTypeAssertion from "#lint/rules/require-safety-comment-for-type-assertion.js";
33
+
34
+ // Stop-slop rules (from eslint-plugin-stop-slop)
35
+ import noAiWords from "#lint/rules/no-ai-words.js";
36
+ import noEmDash from "#lint/rules/no-em-dash.js";
37
+ import noEmoji from "#lint/rules/no-emoji.js";
38
+ import noInlineSvg from "#lint/rules/no-inline-svg.js";
39
+ import noInlinePath from "#lint/rules/no-inline-path.js";
40
+ import noRawTryCatch from "#lint/rules/no-raw-try-catch.js";
41
+ import svelteSectionOrder from "#lint/rules/svelte-section-order.js";
42
+
43
+ // Svelte and SvelteKit house rules
44
+ import noAppStores from "#lint/rules/no-app-stores.js";
45
+ import noRuneOutsideSvelteFile from "#lint/rules/no-rune-outside-svelte-file.js";
46
+ import noRuneShadowingBinding from "#lint/rules/no-rune-shadowing-binding.js";
47
+ import noSkippedOrFocusedTests from "#lint/rules/no-skipped-or-focused-tests.js";
48
+ import noSvelte4Syntax from "#lint/rules/no-svelte4-syntax.js";
49
+ import noVitestImportInVitePlus from "#lint/rules/no-vitest-import-in-vite-plus.js";
50
+ import propsInterface from "#lint/rules/props-interface.js";
51
+ import redirectExternalNeedsOption from "#lint/rules/redirect-external-needs-option.js";
52
+
53
+ import { markdownProcessor } from "#lint/processors/markdown.js";
54
+
55
+ export default {
56
+ meta: {
57
+ name: "@heyramzi/lint",
58
+ version: "2.0.0",
59
+ },
60
+ rules: {
61
+ // Code quality
62
+ "no-relative-imports": noRelativeImports,
63
+ "no-relative-type-imports": noRelativeTypeImports,
64
+ "no-deep-phosphor-imports": noDeepPhosphorImports,
65
+ "no-class": noClass,
66
+ "no-default-export": noDefaultExport,
67
+ "no-overkill-fallback": noOverkillFallback,
68
+ "no-redundant-prop-fallback": noRedundantPropFallback,
69
+ "max-object-methods": maxObjectMethods,
70
+ "max-function-lines": maxFunctionLines,
71
+ "no-snake-case-props": noSnakeCaseProps,
72
+ "no-hardcoded-models": noHardcodedModels,
73
+ "no-legacy-compat": noLegacyCompat,
74
+
75
+ // Convex
76
+ "convex-index-name": convexIndexName,
77
+ "convex-internal-only": convexInternalOnly,
78
+ "convex-no-date-now-in-query": convexNoDateNowInQuery,
79
+ "convex-no-deprecated-crons": convexNoDeprecatedCrons,
80
+ "convex-no-filter": convexNoFilter,
81
+ "convex-no-get-metadata": convexNoGetMetadata,
82
+ "convex-no-v-any": convexNoVAny,
83
+ "convex-react-client-module-scope": convexReactClientModuleScope,
84
+ "convex-require-args": convexRequireArgs,
85
+ "convex-require-returns": convexRequireReturns,
86
+ "convex-runtime-imports": convexRuntimeImports,
87
+
88
+ // Type evidence
89
+ "no-chained-type-assertions": noChainedTypeAssertions,
90
+ "no-conditional-empty-object-spread": noConditionalEmptyObjectSpread,
91
+ "no-known-value-widening": noKnownValueWidening,
92
+ "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertion,
93
+
94
+ // Stop slop
95
+ "no-ai-words": noAiWords,
96
+ "no-em-dash": noEmDash,
97
+ "no-emoji": noEmoji,
98
+ "no-inline-svg": noInlineSvg,
99
+ "no-inline-path": noInlinePath,
100
+ "no-raw-try-catch": noRawTryCatch,
101
+ "svelte-section-order": svelteSectionOrder,
102
+
103
+ // Svelte and SvelteKit
104
+ "no-app-stores": noAppStores,
105
+ "no-rune-outside-svelte-file": noRuneOutsideSvelteFile,
106
+ "no-rune-shadowing-binding": noRuneShadowingBinding,
107
+ "no-skipped-or-focused-tests": noSkippedOrFocusedTests,
108
+ "no-svelte4-syntax": noSvelte4Syntax,
109
+ "no-vitest-import-in-vite-plus": noVitestImportInVitePlus,
110
+ "props-interface": propsInterface,
111
+ "redirect-external-needs-option": redirectExternalNeedsOption,
112
+ },
113
+ processors: {
114
+ markdown: markdownProcessor,
115
+ },
116
+ };
package/inline-path.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The inline path gate: SVG paths must not be embedded inline in components.
3
+ * SVGs must be separate files (.svg) and imported.
4
+ *
5
+ * Pure here, git and process in `bin/heyramzi-inline-path.js`.
6
+ */
7
+
8
+ export const DEFAULT_EXTENSIONS = ["svelte", "vue", "jsx", "tsx", "html", "astro"];
9
+
10
+ /** Generated or vendored trees: nobody can refactor code they do not write. */
11
+ export const DEFAULT_IGNORED = [
12
+ "node_modules",
13
+ "dist",
14
+ "build",
15
+ "builds",
16
+ "out",
17
+ "coverage",
18
+ "vendor",
19
+ ".upstream",
20
+ "generated",
21
+ "_generated",
22
+ "paraglide",
23
+ ".svelte-kit",
24
+ ".wrangler",
25
+ ".vercel",
26
+ ".agents",
27
+ ".claude",
28
+ ".next",
29
+ "static",
30
+ // Boards draw their diagrams inline by the playground skill's rule, and `playground check` gates them.
31
+ "playground",
32
+ ];
33
+
34
+ /**
35
+ * Scans a file's content for inline <path> tags that have static path data (d="...").
36
+ * Returns an array of matches: { line, snippet }.
37
+ */
38
+ export function findInlinePaths(source) {
39
+ const matches = [];
40
+
41
+ // Match <path ... > tags across potential line breaks
42
+ const pathTagRegex = /<path\b([^>]*?)(\/?>)/gis;
43
+ let match;
44
+
45
+ while ((match = pathTagRegex.exec(source)) !== null) {
46
+ // Only a static path data attribute d="..." or d='...' counts
47
+ if (!/\bd\s*=\s*["'][^"']*["']/is.test(match[1])) continue;
48
+
49
+ matches.push({
50
+ line: source.slice(0, match.index).split("\n").length,
51
+ snippet: match[0].slice(0, 80).replace(/\s+/g, " "),
52
+ });
53
+ }
54
+
55
+ return matches;
56
+ }
57
+
58
+ /** Total count of inline static paths in source. */
59
+ export function countInlinePaths(source) {
60
+ return findInlinePaths(source).length;
61
+ }
62
+
63
+ /** A hard rule: every file with an inline static path fails. */
64
+ export function check(counts) {
65
+ const failures = [];
66
+ for (const [file, count] of Object.entries(counts)) {
67
+ if (count > 0) failures.push({ file, count });
68
+ }
69
+ return failures.toSorted((a, b) => b.count - a.count);
70
+ }
71
+
72
+ /** Filter tracked files for the gate. */
73
+ export function selectFiles(
74
+ tracked,
75
+ { targets = [], extensions = DEFAULT_EXTENSIONS, ignored = DEFAULT_IGNORED } = {},
76
+ ) {
77
+ const wanted = new Set(extensions.map((ext) => ext.replace(/^\./, "")));
78
+ return tracked.filter((file) => {
79
+ // Explicitly exclude standalone .svg files
80
+ if (file.endsWith(".svg")) return false;
81
+ const ext = file.slice(file.lastIndexOf(".") + 1);
82
+ if (!wanted.has(ext)) return false;
83
+ if (file.endsWith(".min.js")) return false;
84
+ if (file.split("/").some((segment) => ignored.includes(segment))) return false;
85
+ if (targets.length === 0) return true;
86
+ return targets.some(
87
+ (target) => file === target || file.startsWith(`${target.replace(/\/$/, "")}/`),
88
+ );
89
+ });
90
+ }
package/jev-code.js ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Coding guidelines as tiny Jev questions, judged on the edited code alone after each edit.
3
+ * Only rules a reader can settle from the snippet, no repo context. Bench: scripts/jev-code-bench.mjs.
4
+ */
5
+ import { NO_KEY, judge, q } from "#lint/jev.js";
6
+
7
+ // =========================
8
+ // CONSTANTS
9
+ // =========================
10
+ const TS = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "svelte", "vue"];
11
+ const CODE = [...TS, "swift", "py", "sh"];
12
+ const PLAIN = "Ordinary code that does its job directly, or code this question does not apply to.";
13
+
14
+ /** `langs` scopes a rule by extension; `fix` is what the agent reads; `advisory` never blocks, since acting on it needs the callers. */
15
+ export const RULES = {
16
+ cast: {
17
+ langs: TS,
18
+ fix: "Fix the type at its source instead of asserting it.",
19
+ shape: q(
20
+ "Does `code` use a type assertion (`as any`, `as unknown as X`, `as SomeType` on data from outside, a `!` non-null assertion, `@ts-ignore`) to make the compiler accept a value instead of narrowing or typing it properly?",
21
+ "An assertion or suppression silences the type checker where a real type, a guard or a parse would do.",
22
+ `${PLAIN} \`as const\`, a narrowing \`if\`, or a schema parse are not assertions.`,
23
+ ),
24
+ },
25
+ compat: {
26
+ langs: CODE,
27
+ fix: "Delete the shim and move every caller to the new name.",
28
+ shape: q(
29
+ "Does `code` keep an old name or shape alive for backward compatibility: a deprecated alias or re-export, a wrapper that forwards to the new function, or reading one field under two names (`x.newName ?? x.old_name`)?",
30
+ "A second name, alias, fallback field or forwarding wrapper exists only so old callers keep working.",
31
+ PLAIN,
32
+ ),
33
+ },
34
+ swallowed: {
35
+ langs: CODE,
36
+ fix: "Let the error propagate, or handle it with a real recovery.",
37
+ shape: q(
38
+ "Does `code` catch an error and then drop it: an empty catch, a catch that only logs and carries on, or a fallback value that hides a failure the caller needed to know about?",
39
+ "A failure is caught and silenced, so the program continues as if it had worked.",
40
+ `${PLAIN} A catch that rethrows, returns a typed error, retries, or recovers with a documented reason passes.`,
41
+ ),
42
+ },
43
+ impossible: {
44
+ langs: CODE,
45
+ fix: "Remove the check: the code above already guarantees it.",
46
+ shape: q(
47
+ "Does `code` guard against a case that `code` itself already rules out: a null check on a value it just created, a type check on a literal, a try around code that cannot throw, a default for a parameter that is always passed?",
48
+ "A branch, check or fallback handles a situation the surrounding lines make impossible.",
49
+ `${PLAIN} A check on input from outside (a request, a file, a user, an API) is never impossible.`,
50
+ ),
51
+ },
52
+ wrapper: {
53
+ langs: CODE,
54
+ advisory: true,
55
+ fix: "If its callers can call the real target directly, inline it there.",
56
+ shape: q(
57
+ "Does `code` define a function, class or object that only forwards to one other call with the same arguments, adding no logic, name clarity or type?",
58
+ "A middle man: its whole body is a single pass-through call.",
59
+ PLAIN,
60
+ ),
61
+ },
62
+ speculative: {
63
+ langs: CODE,
64
+ advisory: true,
65
+ fix: "If no caller passes it, delete the option or hook.",
66
+ shape: q(
67
+ "Does `code` add configurability nobody in `code` uses: an options parameter, flag, strategy, plugin hook or generic type parameter with a single value or no reader?",
68
+ "A parameter, option, hook or abstraction exists for a future need, not for anything the code shown does.",
69
+ PLAIN,
70
+ ),
71
+ },
72
+ naming: {
73
+ langs: CODE,
74
+ fix: "Rename it for what it holds or does.",
75
+ shape: q(
76
+ "Does `code` introduce a name that says nothing about what it holds or does: `data`, `result`, `tmp`, `info`, `item2`, `handleStuff`, `utils`, `manager`, `helper`, `doIt`, a single letter outside a short loop?",
77
+ "A newly declared name is generic or meaningless, and a reader has to read the body to learn what it is.",
78
+ `${PLAIN} Conventional short names (\`i\` in a loop, \`e\` for an event, \`err\` in a catch) pass.`,
79
+ ),
80
+ },
81
+ nesting: {
82
+ langs: CODE,
83
+ fix: "Return early with guard clauses and flatten the nesting.",
84
+ shape: q(
85
+ "Does `code` nest conditionals three or more levels deep where early returns or one combined guard would make it flat?",
86
+ "An if inside an if inside an if (or nested if-let) that guard clauses could flatten.",
87
+ PLAIN,
88
+ ),
89
+ },
90
+ narration: {
91
+ langs: CODE,
92
+ fix: "Delete the comment, or replace it with the one trap the code can't show.",
93
+ shape: q(
94
+ "Does `code` contain a comment that repeats what the next line plainly does, or narrates history ('changed from X', 'added for the new flow', 'now uses Y') instead of a reason the code cannot show?",
95
+ "A comment that restates the code or reads as a changelog.",
96
+ `${PLAIN} A comment naming a trap, a constraint, a number's source, a non-obvious why, or a dated fact about an outside service passes. No comment passes.`,
97
+ ),
98
+ },
99
+ leftover: {
100
+ langs: CODE,
101
+ fix: "Delete the debug output or commented-out code.",
102
+ shape: q(
103
+ "Does `code` leave debugging or dead material behind: commented-out code, a stray `console.log`/`print` of an intermediate value, or a `TODO` placeholder where the work should be?",
104
+ "Debug output, dead commented code or a placeholder stub.",
105
+ `${PLAIN} Logging that a CLI or server deliberately emits to its user or log passes.`,
106
+ ),
107
+ },
108
+ workaround: {
109
+ langs: CODE,
110
+ fix: "Fix the cause the comment names, then delete the comment.",
111
+ shape: q(
112
+ "Does `code` contain a comment that admits the code is a stopgap: a workaround kept until something else changes, an old name kept until a rename, or a known-wrong shortcut ('quick fix', 'until X ships', 'keep the old route name')?",
113
+ "A comment that explains a hack instead of the code fixing it.",
114
+ `${PLAIN} A comment naming a fact outside the code (a browser bug, an API that answers 404 since a date, a platform limit) passes.`,
115
+ ),
116
+ },
117
+ fallback: {
118
+ langs: CODE,
119
+ fix: "Require the value and fail loudly when it's missing, or read it from its one source.",
120
+ shape: q(
121
+ "Does `code` fall back when a value it needs is missing: a default for required config or input (`?? \"\"`, `|| 'default'`), trying a second or third source for the same value (`a ?? b ?? c`), or carrying on with a placeholder instead of failing?",
122
+ "A fallback that hides a missing value the program needed.",
123
+ `${PLAIN} A default for a truly optional value (a page size, a label shown blank) or a documented retry passes.`,
124
+ ),
125
+ },
126
+ duplicate: {
127
+ langs: CODE,
128
+ fix: "Extract the shared shape once and call it from both places.",
129
+ shape: q(
130
+ "Does `code` repeat the same block of logic two or more times with only a value or name changed, where one function or loop would serve?",
131
+ "Copy-pasted logic appears twice or more inside `code`.",
132
+ PLAIN,
133
+ ),
134
+ },
135
+ effectSync: {
136
+ langs: ["svelte", "ts", "js"],
137
+ fix: "Use `$derived` for state computed from other state; keep `$effect` for side effects.",
138
+ shape: q(
139
+ "Does `code` use a Svelte `$effect` (or `$:` statement) to assign one piece of state from other state, where `$derived` would compute it?",
140
+ "An effect whose body only sets state from other reactive values.",
141
+ `${PLAIN} An effect that talks to the outside world (DOM, network, storage, timers) passes.`,
142
+ ),
143
+ },
144
+ restates: {
145
+ langs: CODE,
146
+ tests: true,
147
+ fix: "Delete it: assert behaviour through the E2E path, not the implementation's own constants or mocks.",
148
+ shape: q(
149
+ "Is `code` a test that checks the implementation instead of a result: its assertion is only that a mock, spy or `vi.fn()` was called, or its expected value is recomputed with the same formula or constant the code under test uses?",
150
+ "`toHaveBeenCalled` on a mock with no check of what came out, or an expected value built from the implementation's own constant or arithmetic.",
151
+ "A test that feeds real input and checks an observable output or side effect.",
152
+ ),
153
+ },
154
+ };
155
+
156
+ // =========================
157
+ // FUNCTIONS
158
+ // =========================
159
+ const TEST = /(\.|_|\/)(test|spec)s?\.|__tests__\/|\/tests?\//;
160
+
161
+ /** The rules that apply to a file, by extension and whether it is a test. */
162
+ export function rulesFor(file) {
163
+ const ext = file.split(".").pop()?.toLowerCase() ?? "";
164
+ const isTest = TEST.test(file);
165
+ return Object.fromEntries(
166
+ Object.entries(RULES).filter(([, r]) => r.langs.includes(ext) && Boolean(r.tests) === isTest),
167
+ );
168
+ }
169
+
170
+ /** Code longer than MAX_LINES is split so each chunk stays one easy judgement. */
171
+ const MAX_LINES = 60;
172
+
173
+ export function chunks(code) {
174
+ const lines = code.split("\n");
175
+ const out = [];
176
+ for (let i = 0; i < lines.length; i += MAX_LINES)
177
+ out.push(lines.slice(i, i + MAX_LINES).join("\n"));
178
+ return out.filter((c) => c.replace(/\s/g, "").length >= 20);
179
+ }
180
+
181
+ /** Every chunk of `code` against the rules for `file`, split into `findings` and `review`. */
182
+ export async function auditCode(code, file, { key = process.env.KILO_API_KEY } = {}) {
183
+ if (!key) throw new Error(`jev-code ${NO_KEY}`);
184
+ const rules = rulesFor(file);
185
+ const items = chunks(code).map((c) => ({ code: `// ${file.split("/").pop()}\n${c}` }));
186
+ if (!items.length || !Object.keys(rules).length) return { findings: [], review: [] };
187
+ const shapes = Object.fromEntries(Object.entries(rules).map(([id, r]) => [id, r.shape]));
188
+ const { findings, review } = await judge(items, shapes, "code", key);
189
+ const advisory = (h) => RULES[h.id].advisory;
190
+ return {
191
+ findings: findings.filter((h) => !advisory(h)),
192
+ review: [...review, ...findings.filter(advisory)],
193
+ };
194
+ }