@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
@@ -0,0 +1,368 @@
1
+ /**
2
+ * The 30-word comment cap: a comment that outgrows its explanation stops being
3
+ * read. The rule is `ai-doc/references/agents-core.md`.
4
+ *
5
+ * Pure here, git and process in `bin/heyramzi-comment-length.js`.
6
+ */
7
+
8
+ /** Ramzi, 4 Sep 2026: "Maximum should be something like 30 words." */
9
+ export const MAX_WORDS = 30;
10
+
11
+ export const JS_EXTENSIONS = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "svelte"];
12
+
13
+ export const DEFAULT_EXTENSIONS = [
14
+ ...JS_EXTENSIONS,
15
+ "swift",
16
+ "py",
17
+ "sh",
18
+ "bash",
19
+ "zsh",
20
+ "css",
21
+ "sql",
22
+ ];
23
+
24
+ /**
25
+ * Comment syntax for the languages the JS scanner can't read. Backslash escapes, quotes that span
26
+ * lines, and the shell's heredocs and word-start `#` are the traps that make each differ.
27
+ */
28
+ const SYNTAX = {
29
+ swift: {
30
+ lead: "//",
31
+ block: true,
32
+ nested: true,
33
+ quotes: ['"'],
34
+ triple: ['"""'],
35
+ raw: true,
36
+ escapes: true,
37
+ },
38
+ py: { lead: "#", quotes: ["'", '"'], triple: ["'''", '"""'], escapes: true },
39
+ sh: {
40
+ lead: "#",
41
+ wordStart: true,
42
+ quotes: ["'", '"'],
43
+ multiline: true,
44
+ heredoc: true,
45
+ rawSingle: true,
46
+ escapes: true,
47
+ },
48
+ css: { block: true, quotes: ['"', "'"], url: true, escapes: true },
49
+ sql: { lead: "--", block: true, quotes: ["'", '"'], multiline: true },
50
+ };
51
+ const SYNTAX_ALIAS = { bash: "sh", zsh: "sh" };
52
+ const syntaxOf = (extension) => SYNTAX[SYNTAX_ALIAS[extension] ?? extension];
53
+
54
+ /** Generated or vendored trees: nobody can shorten a comment they do not write. */
55
+ export const DEFAULT_IGNORED = [
56
+ "node_modules",
57
+ "dist",
58
+ "build",
59
+ "out",
60
+ "coverage",
61
+ "vendor",
62
+ ".upstream",
63
+ "generated",
64
+ "_generated",
65
+ "paraglide",
66
+ ".svelte-kit",
67
+ ".wrangler",
68
+ ".vercel",
69
+ ];
70
+
71
+ function words(text) {
72
+ return text.trim().split(/\s+/).filter(Boolean).length;
73
+ }
74
+
75
+ /**
76
+ * Every comment in a file, as spans, from a scanner that tracks its state. A
77
+ * comment opener inside a string is code: one `import.meta.glob` swallowed
78
+ * 313 words.
79
+ */
80
+ // Quotes are looked for only in code, so an apostrophe in a comment stays
81
+ // prose, and a markup quote closes at its line end.
82
+ export function commentSpans(src) {
83
+ const spans = [];
84
+ let i = 0;
85
+ while (i < src.length) {
86
+ const two = src.slice(i, i + 2);
87
+ // Outside strings a backslash only escapes inside a regex literal, so `/\/*$/` never opens a comment.
88
+ if (src[i] === "\\") {
89
+ i += 2;
90
+ continue;
91
+ }
92
+ if (two === "//") {
93
+ const end = src.indexOf("\n", i);
94
+ const stop = end === -1 ? src.length : end;
95
+ spans.push({ kind: "line", start: i, end: stop });
96
+ i = stop;
97
+ } else if (two === "/*") {
98
+ const close = src.indexOf("*/", i + 2);
99
+ const stop = close === -1 ? src.length : close + 2;
100
+ spans.push({ kind: "block", start: i, end: stop });
101
+ i = stop;
102
+ } else if (src.startsWith("<!--", i)) {
103
+ const close = src.indexOf("-->", i + 4);
104
+ const stop = close === -1 ? src.length : close + 3;
105
+ spans.push({ kind: "html", start: i, end: stop });
106
+ i = stop;
107
+ } else if (src[i] === "/" && regexStarts(src, i)) {
108
+ i = regexEnd(src, i);
109
+ } else if (src[i] === '"' || src[i] === "'" || src[i] === "`") {
110
+ const quote = src[i];
111
+ let j = i + 1;
112
+ while (j < src.length) {
113
+ if (src[j] === "\\") {
114
+ j += 2;
115
+ continue;
116
+ }
117
+ if (src[j] === quote) {
118
+ j += 1;
119
+ break;
120
+ }
121
+ if (src[j] === "\n" && quote !== "`") {
122
+ break;
123
+ }
124
+ j += 1;
125
+ }
126
+ i = j;
127
+ } else {
128
+ i += 1;
129
+ }
130
+ }
131
+ return mergeRuns(src, spans);
132
+ }
133
+
134
+ /**
135
+ * Consecutive line comments read as one comment: a run that outgrows its point
136
+ * is what this gate is for. A blank line ends a run.
137
+ */
138
+ function mergeRuns(src, spans) {
139
+ const merged = [];
140
+ for (const span of spans) {
141
+ const last = merged[merged.length - 1];
142
+ const gap = last ? src.slice(last.end, span.start) : "";
143
+ const adjacent = last && !gap.trim() && (gap.match(/\n/g) || []).length === 1;
144
+ if (adjacent && last.kind === "line" && span.kind === "line") {
145
+ last.end = span.end;
146
+ continue;
147
+ }
148
+ merged.push({ ...span });
149
+ }
150
+ return merged;
151
+ }
152
+
153
+ /** The comments of a file in `extension`'s language, as spans. */
154
+ export function commentSpansFor(src, extension) {
155
+ const syntax = syntaxOf(extension);
156
+ return syntax ? simpleSpans(src, syntax) : commentSpans(src);
157
+ }
158
+
159
+ function skipString(src, i, syntax) {
160
+ const quote = src[i];
161
+ const triple = syntax.triple?.find((t) => src.startsWith(t, i));
162
+ const escapes = syntax.escapes && !(syntax.rawSingle && quote === "'" && src[i - 1] !== "$");
163
+ let j = i + (triple ? 3 : 1);
164
+ while (j < src.length) {
165
+ if (escapes && src[j] === "\\") {
166
+ j += 2;
167
+ } else if (triple ? src.startsWith(triple, j) : src[j] === quote) {
168
+ return j + (triple ? 3 : 1);
169
+ } else if (src[j] === "\n" && !triple && !syntax.multiline) {
170
+ return j;
171
+ } else {
172
+ j += 1;
173
+ }
174
+ }
175
+ return j;
176
+ }
177
+
178
+ /** Swift `#"..."#`, with as many hashes as it opens with. Null when the `#` isn't a raw string. */
179
+ function rawStringEnd(src, i) {
180
+ let hashes = 0;
181
+ while (src[i + hashes] === "#") hashes += 1;
182
+ if (src[i + hashes] !== '"') return null;
183
+ const close = (src.startsWith('"""', i + hashes) ? '"""' : '"') + "#".repeat(hashes);
184
+ const end = src.indexOf(close, i + hashes + 1);
185
+ return end === -1 ? src.length : end + close.length;
186
+ }
187
+
188
+ const HEREDOC = /^<<(-?)\s*(?:'([^']+)'|"([^"]+)"|\\?([A-Za-z_]\w*))/;
189
+
190
+ /** Past the bodies of the heredocs opened on the line that just ended at `i`. */
191
+ function skipHeredocs(src, i, pending) {
192
+ let j = i;
193
+ for (const { tag, dash } of pending) {
194
+ while (j < src.length) {
195
+ const next = src.indexOf("\n", j + 1);
196
+ const stop = next === -1 ? src.length : next;
197
+ const line = src.slice(j + 1, stop);
198
+ j = stop;
199
+ if ((dash ? line.replace(/^\t+/, "") : line) === tag) break;
200
+ }
201
+ }
202
+ return j;
203
+ }
204
+
205
+ function simpleSpans(src, syntax) {
206
+ const spans = [];
207
+ const pending = [];
208
+ let i = 0;
209
+ while (i < src.length) {
210
+ const c = src[i];
211
+ if (c === "\n" && pending.length) {
212
+ i = skipHeredocs(src, i, pending.splice(0));
213
+ } else if (c === "\\" && syntax.escapes) {
214
+ i += 2;
215
+ } else if (
216
+ syntax.lead &&
217
+ src.startsWith(syntax.lead, i) &&
218
+ (!syntax.wordStart || i === 0 || /[\s;&|(]/.test(src[i - 1]))
219
+ ) {
220
+ const end = src.indexOf("\n", i);
221
+ const stop = end === -1 ? src.length : end;
222
+ spans.push({ kind: "line", lead: syntax.lead, start: i, end: stop });
223
+ i = stop;
224
+ } else if (syntax.block && src.startsWith("/*", i)) {
225
+ let depth = 1;
226
+ let j = i + 2;
227
+ while (j < src.length && depth > 0) {
228
+ if (syntax.nested && src.startsWith("/*", j)) {
229
+ depth += 1;
230
+ j += 2;
231
+ } else if (src.startsWith("*/", j)) {
232
+ depth -= 1;
233
+ j += 2;
234
+ } else {
235
+ j += 1;
236
+ }
237
+ }
238
+ spans.push({ kind: "block", start: i, end: j });
239
+ i = j;
240
+ } else if (syntax.raw && c === "#" && rawStringEnd(src, i) !== null) {
241
+ i = rawStringEnd(src, i);
242
+ } else if (syntax.url && src.startsWith("url(", i) && !/^url\(\s*["']/.test(src.slice(i, i + 12))) {
243
+ const close = src.indexOf(")", i);
244
+ i = close === -1 ? src.length : close + 1;
245
+ } else if (syntax.quotes.includes(c)) {
246
+ i = skipString(src, i, syntax);
247
+ } else {
248
+ // `<<<` is a here-string, and its 2nd and 3rd `<` must not read as a heredoc opener.
249
+ const opener =
250
+ syntax.heredoc && src.startsWith("<<", i) && src[i - 1] !== "<" && src[i + 2] !== "<";
251
+ const heredoc = opener ? HEREDOC.exec(src.slice(i, i + 80)) : null;
252
+ if (heredoc)
253
+ pending.push({ dash: heredoc[1] === "-", tag: heredoc[2] ?? heredoc[3] ?? heredoc[4] });
254
+ i += heredoc ? heredoc[0].length : 1;
255
+ }
256
+ }
257
+ return mergeRuns(src, spans);
258
+ }
259
+
260
+ const REGEX_AFTER = new Set("(,=:[!&|?{};+-*%<>~^");
261
+ const REGEX_KEYWORD = /\b(?:return|typeof|case|in|of|delete|void|throw|new|else|do|yield|await)$/;
262
+
263
+ /** A `/` opens a regex after an operator or keyword, never after a value, so `a / b` stays division. */
264
+ function regexStarts(src, i) {
265
+ const before = src.slice(0, i).trimEnd();
266
+ return before === "" || REGEX_AFTER.has(before[before.length - 1]) || REGEX_KEYWORD.test(before);
267
+ }
268
+
269
+ /** Where a regex literal ends, flags included. A newline first means it was division after all. */
270
+ function regexEnd(src, i) {
271
+ let j = i + 1;
272
+ let inClass = false;
273
+ while (j < src.length) {
274
+ const c = src[j];
275
+ if (c === "\\") j += 2;
276
+ else if (c === "\n") return i + 1;
277
+ else if (c === "[") {
278
+ inClass = true;
279
+ j += 1;
280
+ } else if (c === "]") {
281
+ inClass = false;
282
+ j += 1;
283
+ } else if (c === "/" && !inClass) break;
284
+ else j += 1;
285
+ }
286
+ j += 1;
287
+ while (j < src.length && /[a-z]/i.test(src[j])) j += 1;
288
+ return j;
289
+ }
290
+
291
+ /** A span's prose, with the comment punctuation taken off. */
292
+ export function spanText(src, span) {
293
+ const raw = src.slice(span.start, span.end);
294
+ if (span.kind === "block") return raw.replace(/^\/\*+|\*+\/$/g, "").replace(/^[ \t]*\*/gm, "");
295
+ if (span.kind === "html") return raw.replace(/^<!--|-->$/g, "");
296
+ const lead = (span.lead ?? "//").replace(/[^\w\s]/g, "\\$&");
297
+ return raw.replace(new RegExp(`^[ \\t]*${lead}+\\s?`, "gm"), "");
298
+ }
299
+
300
+ /** The longest comment in a file, in words. Zero when the file has none. */
301
+ export function maxCommentWords(source, extension = "ts") {
302
+ let longest = 0;
303
+ for (const span of commentSpansFor(source, extension)) {
304
+ longest = Math.max(longest, words(spanText(source, span)));
305
+ }
306
+ return longest;
307
+ }
308
+
309
+ /**
310
+ * Words that mark a hack left in place, which agents copy along with the hack.
311
+ * The tags match uppercase only, so "hack" in a sentence stays prose.
312
+ */
313
+ export const MARKERS = [
314
+ { label: "a TODO, FIXME or HACK tag", test: /\b(?:TODO|FIXME|HACK)\b/ },
315
+ { label: '"for now"', test: /\bfor now\b/i },
316
+ {
317
+ label: "a temporary fix",
318
+ test: /\btemporar(?:y|ily)\s+(?:fix|workaround|hack|patch|solution)\b/i,
319
+ },
320
+ { label: "backward compatibility", test: /\bbackwards?[\s-]*compat/i },
321
+ {
322
+ label: "a legacy name or path",
323
+ test: /\blegacy\s+(?:alias|synonym|shim|wrapper|fallback|export|import|path|name|field|support|code|compat)\b/i,
324
+ },
325
+ { label: "a @deprecated tag", test: /@deprecated\b/ },
326
+ ];
327
+
328
+ /** Every marker in a file's comments, with its line. A backticked span is a quote, never a marker. */
329
+ export function markerHits(source, extension = "ts") {
330
+ const hits = [];
331
+ // The markers are a TS, JS and Svelte rule; the other languages only carry the length cap.
332
+ if (!JS_EXTENSIONS.includes(extension)) return hits;
333
+ for (const span of commentSpans(source)) {
334
+ const text = spanText(source, span).replace(/`[^`\n]*`/g, "");
335
+ const marker = MARKERS.find((m) => m.test.test(text));
336
+ if (!marker) continue;
337
+ const line = source.slice(0, span.start).split("\n").length;
338
+ hits.push({ line, label: marker.label });
339
+ }
340
+ return hits;
341
+ }
342
+
343
+ /** A hard cap: every file whose longest comment runs over `max` words fails. */
344
+ export function check(lengths, max = MAX_WORDS) {
345
+ const failures = [];
346
+ for (const [file, count] of Object.entries(lengths)) {
347
+ if (count > max) failures.push({ file, words: count });
348
+ }
349
+ return failures.toSorted((a, b) => b.words - a.words);
350
+ }
351
+
352
+ /** Which of a repo's tracked files the gate reads. `targets` empty means all. */
353
+ export function selectFiles(
354
+ tracked,
355
+ { targets = [], extensions = DEFAULT_EXTENSIONS, ignored = DEFAULT_IGNORED } = {},
356
+ ) {
357
+ const wanted = new Set(extensions.map((ext) => ext.replace(/^\./, "")));
358
+ return tracked.filter((file) => {
359
+ const ext = file.slice(file.lastIndexOf(".") + 1);
360
+ if (!wanted.has(ext)) return false;
361
+ if (file.endsWith(".min.js") || file.endsWith(".min.css")) return false;
362
+ if (file.split("/").some((segment) => ignored.includes(segment))) return false;
363
+ if (targets.length === 0) return true;
364
+ return targets.some(
365
+ (target) => file === target || file.startsWith(`${target.replace(/\/$/, "")}/`),
366
+ );
367
+ });
368
+ }
package/commit-msg.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The commit-message gate: no em or en dash, Conventional Commits and no "!" or BREAKING CHANGE
3
+ * where versions come from commit types. Pure here, git in `bin/heyramzi-commit-msg.js`.
4
+ */
5
+
6
+ const DASH = new RegExp("[\\u2013\\u2014]");
7
+ const SCISSORS = /^# -{24} >8 -{24}$/m;
8
+ const CONVENTIONAL = /^[a-z][a-z0-9-]*(\([^()\r\n]+\))?!?: \S/;
9
+ const GENERATED = /^(Merge |Revert "|fixup! |squash! |amend! )/;
10
+ const BANG = /^[a-z][a-z0-9-]*(\([^()\r\n]+\))?!:/;
11
+ const BREAKING_FOOTER = /^BREAKING[ -]CHANGE:/m;
12
+
13
+ /** What git keeps: no comment lines, nothing after the verbose-commit scissors line. */
14
+ export function messageBody(raw) {
15
+ const cut = SCISSORS.exec(raw);
16
+ const kept = cut ? raw.slice(0, cut.index) : raw;
17
+ return kept
18
+ .split("\n")
19
+ .filter((line) => !line.startsWith("#"))
20
+ .join("\n");
21
+ }
22
+
23
+ /**
24
+ * Problems with a commit message. `conventional` is true in a repo whose versions
25
+ * are cut from the commit types: it also refuses "!" and a BREAKING CHANGE footer.
26
+ */
27
+ export function check(raw, { conventional = false } = {}) {
28
+ const body = messageBody(raw);
29
+ const subject = body.split("\n").find((line) => line.trim()) ?? "";
30
+ const problems = [];
31
+ if (conventional && !GENERATED.test(subject) && !CONVENTIONAL.test(subject)) {
32
+ problems.push(
33
+ `The subject "${subject}" isn't a Conventional Commit. Release-please reads the type to pick the version: type(scope): summary, like fix: or feat(cli): add x. fix and perf are a patch, feat is a minor, and a breaking change is a minor too. Other types (docs, chore, refactor, test) don't release.`,
34
+ );
35
+ }
36
+ if (conventional && !GENERATED.test(subject) && BANG.test(subject)) {
37
+ problems.push(
38
+ `The subject "${subject}" has a "!" after the type. Release-please would cut a major from it. Here a breaking change is a minor: drop the "!" and say what broke in the body. A major needs a "Release-As: X.0.0" footer you write by hand.`,
39
+ );
40
+ }
41
+ if (conventional && BREAKING_FOOTER.test(body)) {
42
+ problems.push(
43
+ `The message has a BREAKING CHANGE footer. Release-please would cut a major from it. Here a breaking change is a minor: say what broke in plain body text instead. A major needs a "Release-As: X.0.0" footer you write by hand.`,
44
+ );
45
+ }
46
+ const dashed = body.split("\n").filter((line) => DASH.test(line));
47
+ if (dashed.length > 0) {
48
+ problems.push(
49
+ `The message has an em or en dash: "${dashed[0].trim()}". Rewrite the sentence with a comma, a colon, parentheses or a full stop. Use a plain hyphen only inside a word.`,
50
+ );
51
+ }
52
+ return problems;
53
+ }
54
+
55
+ export function report(problems) {
56
+ return `commit-msg: the message was refused\n${problems.map((problem) => ` ${problem}`).join("\n")}\n\nEdit the message and commit again.`;
57
+ }
@@ -0,0 +1,87 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxlint-config-schema/refs/heads/main/oxlintrc.schema.json",
3
+ "categories": {
4
+ "correctness": "error",
5
+ "suspicious": "warn",
6
+ "pedantic": "off",
7
+ "perf": "warn",
8
+ "style": "off"
9
+ },
10
+ "plugins": ["typescript", "unicorn", "import", "oxc"],
11
+ "jsPlugins": [".."],
12
+ "ignorePatterns": [
13
+ "**/.agents/**",
14
+ "**/.claude/**",
15
+ "**/.opencode/**",
16
+ "**/_generated/**",
17
+ "**/vendor/**",
18
+ "**/.upstream/**",
19
+ "**/*.generated.*",
20
+ "**/public/**",
21
+ "**/node_modules/**",
22
+ "**/dist/**",
23
+ "build/**",
24
+ "**/.svelte-kit/**",
25
+ "**/.next/**",
26
+ "**/.output/**",
27
+ "**/.vercel/**",
28
+ "**/.wrangler/**",
29
+ "**/coverage/**",
30
+ "**/scripts/secrets.mjs",
31
+ "**/scripts/secrets-auto.mjs",
32
+ "**/scripts/secrets.d.mts"
33
+ ],
34
+ "rules": {
35
+ "eslint/max-lines-per-function": [
36
+ "warn",
37
+ { "max": 150, "skipBlankLines": true, "skipComments": true, "IIFEs": true }
38
+ ],
39
+ "unicorn/no-static-only-class": "error",
40
+ "@heyramzi/lint/no-ai-words": "warn",
41
+ "@heyramzi/lint/no-em-dash": "error",
42
+ "@heyramzi/lint/no-emoji": "error",
43
+ "typescript/no-explicit-any": "error",
44
+ "typescript/ban-ts-comment": "error",
45
+ "typescript/no-non-null-assertion": "error",
46
+ "@heyramzi/lint/no-skipped-or-focused-tests": "error",
47
+ "@heyramzi/lint/no-hardcoded-models": "error",
48
+ "@heyramzi/lint/no-redundant-prop-fallback": "error",
49
+ "@heyramzi/lint/no-app-stores": "error",
50
+ "@heyramzi/lint/no-rune-outside-svelte-file": "error",
51
+ "@heyramzi/lint/no-rune-shadowing-binding": "error",
52
+ "@heyramzi/lint/no-svelte4-syntax": "error",
53
+ "@heyramzi/lint/no-vitest-import-in-vite-plus": "error",
54
+ "@heyramzi/lint/props-interface": "error",
55
+ "@heyramzi/lint/redirect-external-needs-option": "error",
56
+ "@heyramzi/lint/no-raw-try-catch": "warn",
57
+ "@heyramzi/lint/svelte-section-order": "warn",
58
+ "@heyramzi/lint/no-inline-svg": "error",
59
+ "@heyramzi/lint/no-inline-path": "error",
60
+ "@heyramzi/lint/no-relative-imports": "error",
61
+ "@heyramzi/lint/no-relative-type-imports": "off",
62
+ "@heyramzi/lint/no-legacy-compat": "error",
63
+ "@heyramzi/lint/convex-react-client-module-scope": "error",
64
+ "@heyramzi/lint/no-chained-type-assertions": "warn",
65
+ "@heyramzi/lint/no-conditional-empty-object-spread": "warn",
66
+ "@heyramzi/lint/no-known-value-widening": "warn",
67
+ "@heyramzi/lint/require-safety-comment-for-type-assertion": "off"
68
+ },
69
+ "overrides": [
70
+ {
71
+ "files": ["**/convex/**"],
72
+ "rules": {
73
+ "@heyramzi/lint/convex-index-name": "warn",
74
+ "@heyramzi/lint/convex-internal-only": "error",
75
+ "@heyramzi/lint/convex-no-date-now-in-query": "warn",
76
+ "@heyramzi/lint/convex-no-deprecated-crons": "error",
77
+ "@heyramzi/lint/convex-no-filter": "warn",
78
+ "@heyramzi/lint/convex-no-get-metadata": "error",
79
+ "@heyramzi/lint/convex-no-v-any": "warn",
80
+ "@heyramzi/lint/convex-require-args": "error",
81
+ "@heyramzi/lint/convex-require-returns": "warn",
82
+ "@heyramzi/lint/convex-runtime-imports": "error"
83
+ }
84
+ }
85
+ ],
86
+ "options": {}
87
+ }