@heyramzi/lint 1.34.0 → 2.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 (60) hide show
  1. package/README.md +52 -5
  2. package/bin/heyramzi-comment-length.js +5 -6
  3. package/bin/heyramzi-file-tokens.js +2 -2
  4. package/bin/heyramzi-old-names.js +72 -0
  5. package/bin/heyramzi-push-diff.js +16 -5
  6. package/bin/heyramzi-slop.js +3 -1
  7. package/bin/heyramzi-ui-rules.js +18 -9
  8. package/comment-length.js +18 -12
  9. package/config/oxlintrc.json +3 -4
  10. package/data/jev-code-bench.js +5 -5
  11. package/data/slop/artifacts.js +87 -0
  12. package/data/slop/pattern-parts.js +56 -0
  13. package/data/slop/patterns/appositives-and-sayings.js +81 -0
  14. package/data/slop/patterns/comma-clauses.js +106 -0
  15. package/data/slop/patterns/diagram-and-script-tells.js +71 -0
  16. package/data/slop/patterns/dropped-elements.js +149 -0
  17. package/data/slop/patterns/duration-and-openers.js +102 -0
  18. package/data/slop/patterns/headings-and-laws.js +116 -0
  19. package/data/slop/patterns/numbers-and-runs.js +94 -0
  20. package/data/slop/patterns/openers-and-tails.js +90 -0
  21. package/data/slop/patterns/parallel-shapes.js +100 -0
  22. package/data/slop/patterns/reveals-and-tallies.js +101 -0
  23. package/data/slop/patterns/setups-and-closers.js +83 -0
  24. package/data/slop/patterns/subject-voice.js +67 -0
  25. package/data/slop/patterns/vague-and-flourish.js +102 -0
  26. package/data/slop/patterns/wikipedia-signs.js +103 -0
  27. package/data/slop/patterns.js +62 -0
  28. package/data/slop/phrases-en-closers.js +66 -0
  29. package/data/slop/phrases-en-core.js +185 -0
  30. package/data/slop/phrases-en-wikipedia.js +136 -0
  31. package/data/slop/phrases-fr.js +152 -0
  32. package/data/slop/shapes-density.js +81 -0
  33. package/data/slop/shapes-framing.js +101 -0
  34. package/data/slop/shapes-reveals.js +88 -0
  35. package/data/slop/title-tells.js +8 -0
  36. package/data/slop/words-en.js +178 -0
  37. package/data/slop/words-fr.js +101 -0
  38. package/data/slop-words.js +18 -2576
  39. package/data/vendor/heading-words.js +6 -0
  40. package/file-tokens.js +26 -13
  41. package/index.js +4 -6
  42. package/inline-path.js +2 -0
  43. package/jev-code.js +1 -1
  44. package/old-names-mask.js +206 -0
  45. package/old-names.js +145 -0
  46. package/package.json +8 -2
  47. package/push-diff.js +2 -2
  48. package/repo-rules/packages.js +5 -5
  49. package/repo-rules.js +2 -2
  50. package/rules/convex-index-name.js +3 -1
  51. package/rules/convex-internal-only.js +2 -1
  52. package/rules/{convex-no-deprecated-crons.js → convex-no-hourly-daily-crons.js} +8 -7
  53. package/rules/convex-runtime-imports.js +2 -1
  54. package/rules/{no-legacy-compat.js → no-underscore-reexport.js} +1 -1
  55. package/slop.js +4 -2
  56. package/ui-rules-css.js +221 -0
  57. package/ui-rules-markup.js +130 -0
  58. package/ui-rules.js +98 -25
  59. package/vendor/old-names-words.js +9 -0
  60. package/rules/no-relative-type-imports.js +0 -40
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Words a model puts in a section heading, matched against what people type. The old-names gate skips vendor folders.
3
+ */
4
+
5
+ /** The heading word for what a subject leaves behind. */
6
+ export const HERITAGE_HEADING_WORD = "legacy";
package/file-tokens.js CHANGED
@@ -8,8 +8,8 @@ export const MAX_TOKENS = 5000;
8
8
  export const MARKDOWN_MAX_TOKENS = 8000;
9
9
  export const BYTES_PER_TOKEN = 4;
10
10
 
11
- /** Generated, vendored or projected trees: nobody hand-writes these, so nobody splits them. */
12
- export const DEFAULT_IGNORED = [
11
+ /** Generated, vendored or projected trees, and the vendored Obsidian plugin bundles: nobody hand-writes these, so nobody splits them. */
12
+ export const GENERATED_TREES = [
13
13
  "node_modules",
14
14
  "dist",
15
15
  "build",
@@ -31,6 +31,11 @@ export const DEFAULT_IGNORED = [
31
31
  ".codex",
32
32
  ".gemini",
33
33
  ".opencode",
34
+ ".obsidian",
35
+ ];
36
+
37
+ export const DEFAULT_IGNORED = [
38
+ ...GENERATED_TREES,
34
39
  "raw",
35
40
  // Boards are one document per subject by the playground skill's rule, and `playground check` gates them.
36
41
  "playground",
@@ -84,23 +89,31 @@ export function tokens(bytes) {
84
89
  return Math.ceil(bytes / BYTES_PER_TOKEN);
85
90
  }
86
91
 
87
- /** True when the path itself says a machine wrote the file. */
88
- export function ignoredPath(file) {
89
- const parts = file.split("/");
90
- const name = parts.at(-1);
91
- if (LOCKFILES.has(name) || DATA_EXTENSIONS.test(name) || UNCAPPED_EXTENSIONS.test(name))
92
- return true;
93
- if (GENERATED_NAMES.some((pattern) => pattern.test(name))) return true;
94
- if (IGNORED_PREFIXES.some((prefix) => file.startsWith(prefix))) return true;
92
+ function inTrees(parts, trees) {
95
93
  // Match the name as written (`.claude`) and with a symbol prefix stripped; stripping alone dropped every dot-folder.
96
94
  return parts.slice(0, -1).some((part) => {
97
95
  const lower = part.toLowerCase();
98
- return (
99
- DEFAULT_IGNORED.includes(lower) || DEFAULT_IGNORED.includes(lower.replace(/^\W+\s*/u, ""))
100
- );
96
+ return trees.includes(lower) || trees.includes(lower.replace(/^\W+\s*/u, ""));
101
97
  });
102
98
  }
103
99
 
100
+ /** True when the path says a tool wrote or vendored the file: a lockfile, a generated name or a generated tree. */
101
+ export function generatedPath(file) {
102
+ const parts = file.split("/");
103
+ const name = parts.at(-1);
104
+ if (LOCKFILES.has(name)) return true;
105
+ if (GENERATED_NAMES.some((pattern) => pattern.test(name))) return true;
106
+ if (IGNORED_PREFIXES.some((prefix) => file.startsWith(prefix))) return true;
107
+ return inTrees(parts, GENERATED_TREES);
108
+ }
109
+
110
+ /** True when the path says nobody caps the file: generated, data, a page, or a tree of boards and raw sources. */
111
+ export function ignoredPath(file) {
112
+ const name = file.slice(file.lastIndexOf("/") + 1);
113
+ if (DATA_EXTENSIONS.test(name) || UNCAPPED_EXTENSIONS.test(name)) return true;
114
+ return generatedPath(file) || inTrees(file.split("/"), DEFAULT_IGNORED);
115
+ }
116
+
104
117
  /** True when the content says a machine wrote it: a NUL byte (binary, as git decides) or a codegen header. */
105
118
  export function generatedContent(head) {
106
119
  if (head.includes(0)) return true;
package/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // Code quality rules (from @upsys/config)
2
2
  import noRelativeImports from "#lint/rules/no-relative-imports.js";
3
- import noRelativeTypeImports from "#lint/rules/no-relative-type-imports.js";
4
3
  import noDeepPhosphorImports from "#lint/rules/no-deep-phosphor-imports.js";
5
4
  import noClass from "#lint/rules/no-class.js";
6
5
  import noDefaultExport from "#lint/rules/no-default-export.js";
@@ -10,13 +9,13 @@ import maxObjectMethods from "#lint/rules/max-object-methods.js";
10
9
  import maxFunctionLines from "#lint/rules/max-function-lines.js";
11
10
  import noSnakeCaseProps from "#lint/rules/no-snake-case-props.js";
12
11
  import noHardcodedModels from "#lint/rules/no-hardcoded-models.js";
13
- import noLegacyCompat from "#lint/rules/no-legacy-compat.js";
12
+ import noUnderscoreReexport from "#lint/rules/no-underscore-reexport.js";
14
13
 
15
14
  // Convex rules (the checks @convex-dev/eslint-plugin never ran here, plus the house rules)
16
15
  import convexIndexName from "#lint/rules/convex-index-name.js";
17
16
  import convexInternalOnly from "#lint/rules/convex-internal-only.js";
18
17
  import convexNoDateNowInQuery from "#lint/rules/convex-no-date-now-in-query.js";
19
- import convexNoDeprecatedCrons from "#lint/rules/convex-no-deprecated-crons.js";
18
+ import convexNoHourlyDailyCrons from "#lint/rules/convex-no-hourly-daily-crons.js";
20
19
  import convexNoFilter from "#lint/rules/convex-no-filter.js";
21
20
  import convexNoGetMetadata from "#lint/rules/convex-no-get-metadata.js";
22
21
  import convexNoVAny from "#lint/rules/convex-no-v-any.js";
@@ -60,7 +59,6 @@ export default {
60
59
  rules: {
61
60
  // Code quality
62
61
  "no-relative-imports": noRelativeImports,
63
- "no-relative-type-imports": noRelativeTypeImports,
64
62
  "no-deep-phosphor-imports": noDeepPhosphorImports,
65
63
  "no-class": noClass,
66
64
  "no-default-export": noDefaultExport,
@@ -70,13 +68,13 @@ export default {
70
68
  "max-function-lines": maxFunctionLines,
71
69
  "no-snake-case-props": noSnakeCaseProps,
72
70
  "no-hardcoded-models": noHardcodedModels,
73
- "no-legacy-compat": noLegacyCompat,
71
+ "no-underscore-reexport": noUnderscoreReexport,
74
72
 
75
73
  // Convex
76
74
  "convex-index-name": convexIndexName,
77
75
  "convex-internal-only": convexInternalOnly,
78
76
  "convex-no-date-now-in-query": convexNoDateNowInQuery,
79
- "convex-no-deprecated-crons": convexNoDeprecatedCrons,
77
+ "convex-no-hourly-daily-crons": convexNoHourlyDailyCrons,
80
78
  "convex-no-filter": convexNoFilter,
81
79
  "convex-no-get-metadata": convexNoGetMetadata,
82
80
  "convex-no-v-any": convexNoVAny,
package/inline-path.js CHANGED
@@ -16,6 +16,8 @@ export const DEFAULT_IGNORED = [
16
16
  "out",
17
17
  "coverage",
18
18
  "vendor",
19
+ // What a client sent us, kept as it came: company/clients/<slug>/inputs/.
20
+ "inputs",
19
21
  ".upstream",
20
22
  "generated",
21
23
  "_generated",
package/jev-code.js CHANGED
@@ -22,7 +22,7 @@ export const RULES = {
22
22
  `${PLAIN} \`as const\`, a narrowing \`if\`, or a schema parse are not assertions.`,
23
23
  ),
24
24
  },
25
- compat: {
25
+ oldname: {
26
26
  langs: CODE,
27
27
  fix: "Delete the shim and move every caller to the new name.",
28
28
  shape: q(
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Code with comments and strings blanked to spaces at the same length, so a mask offset is a file
3
+ * offset. `old-names.js` reads names from what's left.
4
+ */
5
+
6
+ import { commentSpansFor, regexEnd, regexStarts } from "#lint/comment-length.js";
7
+
8
+ const JS_FAMILY = new Set(["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"]);
9
+
10
+ function blanker(src) {
11
+ const out = src.split("");
12
+ const blank = (from, to) => {
13
+ for (let k = from; k < Math.min(to, out.length); k += 1) if (out[k] !== "\n") out[k] = " ";
14
+ };
15
+ return { out, blank, text: () => out.join("") };
16
+ }
17
+
18
+ /** Comments, quoted strings, template text and regex literals. Code inside `${}` stays. */
19
+ export function maskJs(src) {
20
+ const { blank, text } = blanker(src);
21
+ const resume = [];
22
+ let depth = 0;
23
+ let i = 0;
24
+
25
+ const template = (start) => {
26
+ let j = start;
27
+ while (j < src.length) {
28
+ if (src[j] === "\\") j += 2;
29
+ else if (src[j] === "`") {
30
+ blank(start, j);
31
+ return j + 1;
32
+ } else if (src[j] === "$" && src[j + 1] === "{") {
33
+ blank(start, j + 2);
34
+ resume.push(depth);
35
+ depth += 1;
36
+ return j + 2;
37
+ } else j += 1;
38
+ }
39
+ blank(start, src.length);
40
+ return src.length;
41
+ };
42
+
43
+ while (i < src.length) {
44
+ const c = src[i];
45
+ const two = src.slice(i, i + 2);
46
+ if (two === "//") {
47
+ const end = src.indexOf("\n", i);
48
+ const stop = end === -1 ? src.length : end;
49
+ blank(i, stop);
50
+ i = stop;
51
+ } else if (two === "/*") {
52
+ const close = src.indexOf("*/", i + 2);
53
+ const stop = close === -1 ? src.length : close + 2;
54
+ blank(i, stop);
55
+ i = stop;
56
+ } else if (c === "`") {
57
+ i = template(i + 1);
58
+ } else if (c === '"' || c === "'") {
59
+ let j = i + 1;
60
+ while (j < src.length && src[j] !== c && src[j] !== "\n") j += src[j] === "\\" ? 2 : 1;
61
+ blank(i + 1, j);
62
+ i = j + 1;
63
+ } else if (c === "/" && src[i - 1] !== "<" && regexStarts(src, i)) {
64
+ const stop = regexEnd(src, i);
65
+ blank(i, stop);
66
+ i = stop;
67
+ } else if (c === "{") {
68
+ depth += 1;
69
+ i += 1;
70
+ } else if (c === "}") {
71
+ depth -= 1;
72
+ i = resume.length > 0 && depth === resume.at(-1) ? (resume.pop(), template(i + 1)) : i + 1;
73
+ } else {
74
+ i += 1;
75
+ }
76
+ }
77
+ return text();
78
+ }
79
+
80
+ /** Swift, Python, shell, CSS and SQL, through the comment gate's scanner. SQL double quotes name columns, so they stay. */
81
+ export function maskSimple(src, extension) {
82
+ const { blank, text } = blanker(src);
83
+ const strings = [];
84
+ const comments = commentSpansFor(src, extension, strings);
85
+ for (const span of comments) blank(span.start, span.end);
86
+ for (const span of strings) {
87
+ if (extension === "sql" && src[span.start] === '"') continue;
88
+ blank(span.start, span.end);
89
+ }
90
+ return text();
91
+ }
92
+
93
+ /** JSON and JSONC: keys are names, every value and comment is data. */
94
+ export function maskJson(src) {
95
+ const { blank, text } = blanker(src);
96
+ const token = /("(?:[^"\\\n]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g;
97
+ for (const match of src.matchAll(token)) {
98
+ const end = match.index + match[0].length;
99
+ const isKey = match[1] && /^\s*:/.test(src.slice(end, end + 40));
100
+ if (!isKey) blank(match.index, end);
101
+ }
102
+ return text();
103
+ }
104
+
105
+ const YAML_KEY = /^(\s*(?:-\s+)*)("[^"\n]+"|'[^'\n]+'|[\w./@$-][\w./@$ -]*?)(\s*:)(?=\s|$)/;
106
+ const TOML_KEY = /^(\s*)("[^"\n]+"|'[^'\n]+'|[\w.-]+(?:\s*\.\s*[\w-]+)*)(\s*=)/;
107
+ const TOML_TABLE = /^(\s*\[+\s*)([^\]\n]+)(\]+)/;
108
+
109
+ /** YAML and TOML: a key (or a TOML table header) is a name, the rest of the line is data. */
110
+ export function maskKeyed(src, extension) {
111
+ const keyed = extension === "toml" ? [TOML_TABLE, TOML_KEY] : [YAML_KEY];
112
+ return src
113
+ .split("\n")
114
+ .map((line) => {
115
+ for (const pattern of keyed) {
116
+ const match = pattern.exec(line);
117
+ if (!match) continue;
118
+ const kept = match[1].length + match[2].length;
119
+ const head = " ".repeat(match[1].length) + line.slice(match[1].length, kept);
120
+ return head.padEnd(line.length, " ");
121
+ }
122
+ return " ".repeat(line.length);
123
+ })
124
+ .join("\n");
125
+ }
126
+
127
+ const TAG_NAME = /^<\/?[A-Za-z][\w:.-]*/;
128
+
129
+ /** The end of a `{...}` expression that starts at `i`, counting nested braces and skipping quoted braces. */
130
+ function braceEnd(src, i) {
131
+ let depth = 0;
132
+ for (let j = i; j < src.length; j += 1) {
133
+ const c = src[j];
134
+ if (c === '"' || c === "'" || c === "`") {
135
+ const close = src.indexOf(c, j + 1);
136
+ if (close !== -1) j = close;
137
+ } else if (c === "{") depth += 1;
138
+ else if (c === "}" && (depth -= 1) === 0) return j + 1;
139
+ }
140
+ return src.length;
141
+ }
142
+
143
+ /** The end of a tag opened at `i`: quoted attribute values are blanked, `{...}` expressions stay. */
144
+ function tagEnd(src, i, blank, braces) {
145
+ let j = i + 1;
146
+ while (j < src.length && src[j] !== ">") {
147
+ const c = src[j];
148
+ if (c === '"' || c === "'") {
149
+ const close = src.indexOf(c, j + 1);
150
+ const stop = close === -1 ? src.length : close;
151
+ blank(j + 1, stop);
152
+ j = stop + 1;
153
+ } else if (c === "{" && braces) {
154
+ j = braceEnd(src, j);
155
+ } else j += 1;
156
+ }
157
+ return j + 1;
158
+ }
159
+
160
+ /** The inside of a `<script>` or `<style>` element, masked as its own language. Returns where the element's code ends. */
161
+ function maskBlock(src, out, from, name) {
162
+ const close = src.toLowerCase().indexOf(`</${name}`, from);
163
+ const stop = close === -1 ? src.length : close;
164
+ const body = src.slice(from, stop);
165
+ const inner = name === "style" ? maskSimple(body, "css") : maskJs(body);
166
+ for (let k = 0; k < inner.length; k += 1) out[from + k] = inner[k];
167
+ return stop;
168
+ }
169
+
170
+ /**
171
+ * Svelte and HTML. Tag names, attribute names and `{...}` expressions stay, and text, comments and
172
+ * quoted attribute values go. `<script>` is read as JS and `<style>` as CSS.
173
+ */
174
+ export function maskMarkup(src, { braces }) {
175
+ const { out, blank, text } = blanker(src);
176
+ let i = 0;
177
+ while (i < src.length) {
178
+ if (src.startsWith("<!--", i)) {
179
+ const close = src.indexOf("-->", i + 4);
180
+ const stop = close === -1 ? src.length : close + 3;
181
+ blank(i, stop);
182
+ i = stop;
183
+ } else if (src[i] === "<" && TAG_NAME.test(src.slice(i, i + 40))) {
184
+ const name = /^<([A-Za-z]+)/.exec(src.slice(i, i + 12))?.[1]?.toLowerCase();
185
+ i = tagEnd(src, i, blank, braces);
186
+ if (name === "script" || name === "style") i = maskBlock(src, out, i, name);
187
+ } else if (src[i] === "{" && braces) {
188
+ i = braceEnd(src, i);
189
+ } else {
190
+ blank(i, i + 1);
191
+ i += 1;
192
+ }
193
+ }
194
+ return text();
195
+ }
196
+
197
+ /** The code of a file with comments and values blanked, or null when the language isn't one we read. */
198
+ export function maskFor(src, extension) {
199
+ if (JS_FAMILY.has(extension)) return maskJs(src);
200
+ if (extension === "svelte") return maskMarkup(src, { braces: true });
201
+ if (extension === "html") return maskMarkup(src, { braces: false });
202
+ if (extension === "json" || extension === "jsonc") return maskJson(src);
203
+ if (extension === "yaml" || extension === "yml" || extension === "toml")
204
+ return maskKeyed(src, extension);
205
+ return maskSimple(src, extension === "bash" || extension === "zsh" ? "sh" : extension);
206
+ }
package/old-names.js ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The old-names gate (Ramzi, 22 Oct 2026). The words sit in `vendor/old-names-words.js`, which it
3
+ * skips. Git and process live in `bin/heyramzi-old-names.js`.
4
+ */
5
+
6
+ import { generatedContent, generatedPath } from "#lint/file-tokens.js";
7
+ import { maskFor } from "#lint/old-names-mask.js";
8
+ import { PART_WORDS, SUBSTRING_WORD } from "#lint/vendor/old-names-words.js";
9
+
10
+ export const OLD_NAME_EXTENSIONS = [
11
+ "ts",
12
+ "tsx",
13
+ "mts",
14
+ "cts",
15
+ "js",
16
+ "jsx",
17
+ "mjs",
18
+ "cjs",
19
+ "svelte",
20
+ "swift",
21
+ "py",
22
+ "sql",
23
+ "sh",
24
+ "bash",
25
+ "zsh",
26
+ "css",
27
+ "html",
28
+ "json",
29
+ "jsonc",
30
+ "yaml",
31
+ "yml",
32
+ "toml",
33
+ ];
34
+
35
+ /** Applied migrations are history the database already ran, and drizzle-kit writes its output folder whole. */
36
+ const MIGRATION_TREES = new Set(["migrations", "drizzle"]);
37
+
38
+ /** Which of a repo's tracked files the gate reads: its own code, not a tool's output or a migration that already ran. */
39
+ export function selectFiles(tracked) {
40
+ const wanted = new Set(OLD_NAME_EXTENSIONS);
41
+ return tracked.filter((file) => {
42
+ if (!wanted.has(file.slice(file.lastIndexOf(".") + 1))) return false;
43
+ if (generatedPath(file) || /\.min\.(js|css)$/.test(file)) return false;
44
+ return !file
45
+ .split("/")
46
+ .slice(0, -1)
47
+ .some((part) => MIGRATION_TREES.has(part));
48
+ });
49
+ }
50
+
51
+ /** A name's word parts: `toCompat`, `api-shim` and `DEPRECATED_FIELD` all split where a person would. */
52
+ function partsOf(token) {
53
+ return (token.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|\d+/g) ?? []).map((part) => part.toLowerCase());
54
+ }
55
+
56
+ function bannedWords(token) {
57
+ const found = [];
58
+ if (token.toLowerCase().includes(SUBSTRING_WORD)) found.push(SUBSTRING_WORD);
59
+ for (const part of partsOf(token)) {
60
+ const base = part.replace(/s$/, "");
61
+ if (PART_WORDS.includes(base) && !found.includes(base)) found.push(base);
62
+ }
63
+ return found;
64
+ }
65
+
66
+ /** The directory and file names on a path that carry a banned word. A folder shows once, at the folder. */
67
+ export function nameHits(file) {
68
+ const parts = file.split("/");
69
+ return parts.flatMap((part, index) =>
70
+ bannedWords(part).map((banned) => ({
71
+ file: parts.slice(0, index + 1).join("/"),
72
+ line: 0,
73
+ word: banned,
74
+ token: part,
75
+ folder: index < parts.length - 1,
76
+ })),
77
+ );
78
+ }
79
+
80
+ const TOKEN = /[\w$-]+/g;
81
+
82
+ /** Every banned word in a file's text, with its line. The substring word anywhere, the part words in code. */
83
+ export function sourceHits(source, extension) {
84
+ const hits = [];
85
+ const seen = new Set();
86
+ const lines = source.split("\n");
87
+ const add = (line, banned, token) => {
88
+ const key = `${line}:${banned}:${token}`;
89
+ if (seen.has(key)) return;
90
+ seen.add(key);
91
+ hits.push({ line, word: banned, token, text: lines[line - 1].trim().slice(0, 100) });
92
+ };
93
+ if (source.toLowerCase().includes(SUBSTRING_WORD)) {
94
+ lines.forEach((text, index) => {
95
+ for (const token of text.match(TOKEN) ?? []) {
96
+ if (bannedWords(token).includes(SUBSTRING_WORD)) add(index + 1, SUBSTRING_WORD, token);
97
+ }
98
+ });
99
+ }
100
+ const masked = maskFor(source, extension);
101
+ masked.split("\n").forEach((text, index) => {
102
+ for (const token of text.match(TOKEN) ?? []) {
103
+ for (const banned of bannedWords(token)) {
104
+ if (banned !== SUBSTRING_WORD) add(index + 1, banned, token);
105
+ }
106
+ }
107
+ });
108
+ return hits.toSorted((a, b) => a.line - b.line);
109
+ }
110
+
111
+ /** Every hit in one file: its path first, then its text. A file a tool wrote, by its header, has none. */
112
+ export function scanFile(file, source) {
113
+ if (generatedContent(Buffer.from(source.slice(0, 2048)))) return [];
114
+ const extension = file.slice(file.lastIndexOf(".") + 1);
115
+ const inText = sourceHits(source, extension);
116
+ for (const hit of inText) hit.file = file;
117
+ return [...nameHits(file), ...inText];
118
+ }
119
+
120
+ /** Hits over many files, `sources` mapping path to text. A folder is named once, however many files sit in it. */
121
+ export function check(sources) {
122
+ const hits = [];
123
+ const named = new Set();
124
+ for (const [file, source] of Object.entries(sources)) {
125
+ for (const hit of scanFile(file, source)) {
126
+ const key = `${hit.file}:${hit.word}:${hit.line}:${hit.token}`;
127
+ if (hit.line === 0 && named.has(key)) continue;
128
+ named.add(key);
129
+ hits.push(hit);
130
+ }
131
+ }
132
+ return hits;
133
+ }
134
+
135
+ const ADVICE =
136
+ "Delete the old path and move every caller to the new one, or rename the thing to what it is now. A refactor leaves no alias export, no old field kept for a migration and no shim. There's no allowlist.";
137
+
138
+ /** The refusal. Each hit names the token, and the footer says what to do instead. */
139
+ export function report(hits) {
140
+ const lines = hits.map(({ file, line, word: banned, token, text, folder }) => {
141
+ if (line > 0) return `${file}:${line}: "${banned}" in ${token}: ${text}`;
142
+ return `${file}${folder ? "/" : ""}: "${banned}" in the ${folder ? "folder" : "file"} name ${token}`;
143
+ });
144
+ return [`old-names: ${hits.length} hit(s)`, ...lines, "", ADVICE].join("\n");
145
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/lint",
3
- "version": "1.34.0",
3
+ "version": "2.1.0",
4
4
  "description": "Shared oxlint rules, base config and gates for Studio projects",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,11 +9,12 @@
9
9
  "directory": "packages/lint"
10
10
  },
11
11
  "bin": {
12
- "heyramzi-commit-msg": "./bin/heyramzi-commit-msg.js",
13
12
  "heyramzi-comment-length": "./bin/heyramzi-comment-length.js",
13
+ "heyramzi-commit-msg": "./bin/heyramzi-commit-msg.js",
14
14
  "heyramzi-file-tokens": "./bin/heyramzi-file-tokens.js",
15
15
  "heyramzi-inline-path": "./bin/heyramzi-inline-path.js",
16
16
  "heyramzi-jev-code": "./bin/heyramzi-jev-code.js",
17
+ "heyramzi-old-names": "./bin/heyramzi-old-names.js",
17
18
  "heyramzi-push-diff": "./bin/heyramzi-push-diff.js",
18
19
  "heyramzi-repo-rules": "./bin/heyramzi-repo-rules.js",
19
20
  "heyramzi-slop": "./bin/heyramzi-slop.js",
@@ -32,6 +33,8 @@
32
33
  "index.js",
33
34
  "jev.js",
34
35
  "jev-code.js",
36
+ "old-names.js",
37
+ "old-names-mask.js",
35
38
  "package.json",
36
39
  "processors",
37
40
  "push-diff.js",
@@ -39,6 +42,8 @@
39
42
  "slop.js",
40
43
  "slop-views.js",
41
44
  "ui-rules.js",
45
+ "ui-rules-css.js",
46
+ "ui-rules-markup.js",
42
47
  "svelte-markup.js",
43
48
  "svelte-template.js",
44
49
  "register.js",
@@ -46,6 +51,7 @@
46
51
  "repo-rules.js",
47
52
  "repo-rules",
48
53
  "scripts",
54
+ "vendor",
49
55
  "rules"
50
56
  ],
51
57
  "type": "module",
package/push-diff.js CHANGED
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import { commentSpans } from "#lint/comment-length.js";
6
6
 
7
-
8
7
  /** The registry the add-ons persist under: `enum StorageKey {…}` or `const StorageKey = {…}`. */
9
8
  const REGISTRY = /(?:\benum\s+StorageKey\s*\{|\bStorageKey\s*(?::[^=\n]+)?=\s*\{)/;
10
9
 
@@ -27,7 +26,8 @@ export function storageKeysOf(source) {
27
26
  end += 1;
28
27
  }
29
28
  const body = clean.slice(open.index + open[0].length, end);
30
- const entry = /(?:^|[\s,{])(["']?)([A-Za-z_$][\w$]*)\1\s*[:=]\s*(["'`])((?:\\.|(?!\3)[^\\\n])*)\3/g;
29
+ const entry =
30
+ /(?:^|[\s,{])(["']?)([A-Za-z_$][\w$]*)\1\s*[:=]\s*(["'`])((?:\\.|(?!\3)[^\\\n])*)\3/g;
31
31
  // A template that only splices string literals, `a_${"b"}`, is the same string at runtime.
32
32
  const literal = (value) => value.replace(/\$\{\s*(["'])((?:(?!\1).)*)\1\s*\}/g, "$2");
33
33
  for (const match of body.matchAll(entry)) keys.set(match[2], literal(match[4]));
@@ -141,22 +141,22 @@ export function npmrcCredentials(tree) {
141
141
  return found;
142
142
  }
143
143
 
144
- const LEGACY_TOKEN = ["GITHUB", "PRIVATE", "PACKAGES", "TOKEN"].join("_");
144
+ const DEAD_TOKEN = ["GITHUB", "PRIVATE", "PACKAGES", "TOKEN"].join("_");
145
145
  const INSTALL_FILES =
146
146
  /(^|\/)(\.npmrc|\.env[^/]*|Dockerfile[^/]*|(docker-)?compose[^/]*\.ya?ml|wrangler\.jsonc?|[^/]+\.sh|\.github\/workflows\/[^/]+\.ya?ml)$/;
147
147
 
148
- export function legacyTokenName(tree) {
148
+ export function deadTokenName(tree) {
149
149
  const found = [];
150
150
  for (const file of tree.touchedMatching(INSTALL_FILES)) {
151
151
  tree
152
152
  .text(file)
153
153
  .split("\n")
154
154
  .forEach((line, index) => {
155
- if (!line.includes(LEGACY_TOKEN)) return;
155
+ if (!line.includes(DEAD_TOKEN)) return;
156
156
  found.push({
157
157
  file,
158
- rule: "legacy-token-name",
159
- message: `Line ${index + 1} names ${LEGACY_TOKEN}, a dead variable that 403s against the package registry. The package-install token is GH_PRIVATE_PACKAGES_TOKEN everywhere: .npmrc, workflow env, the shell. Leave secrets.GITHUB_TOKEN alone, it's the Actions built-in that release-please, GoReleaser and gh need.`,
158
+ rule: "dead-token-name",
159
+ message: `Line ${index + 1} names ${DEAD_TOKEN}, a dead variable that 403s against the package registry. The package-install token is GH_PRIVATE_PACKAGES_TOKEN everywhere: .npmrc, workflow env, the shell. Leave secrets.GITHUB_TOKEN alone, it's the Actions built-in that release-please, GoReleaser and gh need.`,
160
160
  });
161
161
  });
162
162
  }
package/repo-rules.js CHANGED
@@ -7,7 +7,7 @@ import { composeImages, fossilNames } from "#lint/repo-rules/paths.js";
7
7
  import {
8
8
  heyramziLinks,
9
9
  heyramziTarballs,
10
- legacyTokenName,
10
+ deadTokenName,
11
11
  npmrcCredentials,
12
12
  pnpmOnly,
13
13
  srcImportMaps,
@@ -28,7 +28,7 @@ export const RULES = [
28
28
  heyramziTarballs,
29
29
  srcImportMaps,
30
30
  npmrcCredentials,
31
- legacyTokenName,
31
+ deadTokenName,
32
32
  workflows,
33
33
  foreignTools,
34
34
  fallowShape,
@@ -6,7 +6,9 @@ import { propertyName } from "#lint/rules/convex-ast.js";
6
6
  export default {
7
7
  meta: {
8
8
  type: "problem",
9
- docs: { description: "Require index names to start with `by_` and list every field with `_and_`." },
9
+ docs: {
10
+ description: "Require index names to start with `by_` and list every field with `_and_`.",
11
+ },
10
12
  messages: {
11
13
  badName:
12
14
  'Index "{{ name }}" covers {{ count }} field(s) but its name lists {{ listed }}. Name it `by_` plus every field joined with `_and_`, like `by_channel_and_author`, and update its `withIndex` calls.',
@@ -29,7 +29,8 @@ export default {
29
29
  create(context) {
30
30
  const jobs = new Set(["crons"]);
31
31
  let httpDepth = 0;
32
- const report = (node, where) => context.report({ node, messageId: "publicRef", data: { where } });
32
+ const report = (node, where) =>
33
+ context.report({ node, messageId: "publicRef", data: { where } });
33
34
  return {
34
35
  VariableDeclarator(node) {
35
36
  const init = node.init;
@@ -1,19 +1,20 @@
1
1
  /**
2
- * `crons.hourly` and `crons.daily` are the deprecated helpers.
2
+ * `crons.hourly` and `crons.daily` are the helpers Convex replaced.
3
3
  */
4
4
  import { propertyName } from "#lint/rules/convex-ast.js";
5
5
 
6
- const DEPRECATED = new Set(["hourly", "daily"]);
6
+ const REPLACED_HELPERS = new Set(["hourly", "daily"]);
7
7
 
8
8
  export default {
9
9
  meta: {
10
10
  type: "problem",
11
11
  docs: {
12
- description: "Disallow the deprecated cron helpers. Use `crons.interval` or `crons.cron`.",
12
+ description:
13
+ "Disallow `crons.hourly` and `crons.daily`. Use `crons.interval` or `crons.cron`.",
13
14
  },
14
15
  messages: {
15
- deprecated:
16
- "`crons.{{ method }}` is deprecated. Use `crons.cron(name, '30 3 * * *', internal.x.y, {})` or `crons.interval(name, { hours: 6 }, internal.x.y, {})`. Both run in UTC.",
16
+ replacedHelper:
17
+ "`crons.{{ method }}` is a replaced helper. Use `crons.cron(name, '30 3 * * *', internal.x.y, {})` or `crons.interval(name, { hours: 6 }, internal.x.y, {})`. Both run in UTC.",
17
18
  },
18
19
  schema: [],
19
20
  },
@@ -29,10 +30,10 @@ export default {
29
30
  },
30
31
  CallExpression(node) {
31
32
  const method = propertyName(node.callee);
32
- if (!method || !DEPRECATED.has(method)) return;
33
+ if (!method || !REPLACED_HELPERS.has(method)) return;
33
34
  const owner = node.callee.object;
34
35
  if (owner.type === "Identifier" && jobs.has(owner.name)) {
35
- context.report({ node, messageId: "deprecated", data: { method } });
36
+ context.report({ node, messageId: "replacedHelper", data: { method } });
36
37
  }
37
38
  },
38
39
  };