@heyramzi/lint 0.0.0-stage → 1.32.4

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 (92) 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 +81 -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 +198 -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-views.js +122 -0
  88. package/slop.d.ts +53 -0
  89. package/slop.js +534 -0
  90. package/svelte-markup.js +107 -0
  91. package/svelte-template.js +258 -0
  92. package/ui-rules.js +324 -0
package/jev.js ADDED
@@ -0,0 +1,341 @@
1
+ /**
2
+ * The judgement half of the humanizer patterns, asked of TypeSafe Jev per sentence
3
+ * and per paragraph, through the Kilo gateway. Bench: scripts/jev-bench.mjs. Why these shapes: TECHLOG.md.
4
+ */
5
+
6
+ import { execFileSync } from "node:child_process";
7
+ import { createHash } from "node:crypto";
8
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ // =========================
13
+ // CONSTANTS
14
+ // =========================
15
+ /** TypeSafe's /v1/systemone behind Kilo, which bills our Kilo credits and pins the model. */
16
+ const KILO_URL = "https://api.kilo.ai/api/gateway/typesafe/v1/systemone";
17
+ /** One request that hangs must not hang a draft: past this it counts as unreachable. */
18
+ const TIMEOUT_MS = 30_000;
19
+ /** Kilo only takes the pinned name; `jev-latest` answers 400 model_not_found. */
20
+ export const MODEL = "typesafe/jev-1.13";
21
+ export const NO_KEY = "needs KILO_API_KEY (Proton Pass, Dev vault, item Kilo)";
22
+ export const FAIL = 0.75;
23
+ export const REVIEW = 0.6;
24
+ /** Scores drift about 0.05 between identical calls, so each item is asked RUNS times and its worst score counts. */
25
+ const RUNS = 3;
26
+ /** Items per request: 4 sentences x 9 shapes answered in about 1s. */
27
+ const BATCH = 4;
28
+ const CONCURRENCY = 8;
29
+
30
+ /** One question per root cause, not per pattern: humanizer references/patterns.md maps each pattern to one. */
31
+ export const q = (instructions, yes, no) => ({
32
+ type: "noul",
33
+ instructions,
34
+ criteria: { true: yes, false: no },
35
+ });
36
+ const PLAIN =
37
+ "A plain, literal sentence: a step the reader takes, a named thing, a number, or a fact that carries its own meaning.";
38
+
39
+ /** Asked of every sentence, with its paragraph as `context`. */
40
+ export const SHAPES = {
41
+ inflated: q(
42
+ "Does `sentence` tell the reader that something matters, impresses or feels a certain way, instead of giving the fact, number or mechanism that would show it, or is it generic enough to move unchanged to another company, product or person?",
43
+ "Praise, significance, emotion or quality asserted, not shown, or a line with nothing only this subject could say.",
44
+ `${PLAIN} Or a letter's courtesy to its reader: a thanks, a greeting, an invitation.`,
45
+ ),
46
+ aphorism: q(
47
+ "Is `sentence` a slogan or maxim: a quotable general truth stated in abstract terms instead of a concrete step, number or fact?",
48
+ "Could be printed on a poster, often a flip or an 'only', and the next sentence has to explain it.",
49
+ PLAIN,
50
+ ),
51
+ law: q(
52
+ "Does `sentence` describe how a tool, setting or system behaves as a general rule, with the thing as the subject and no person acting, where it could tell the reader what to do and what they will see?",
53
+ "The subject is an object or an idea, the verb is a state or a spread, and the reader has no step to copy. Often it ends on 'it' or 'them'.",
54
+ "A person does something, a literal step (where to click, what appears), a measured result of what happened, with its number, or a plain fact or rule about one specific thing said the way a person would ('the back door doesn't lock', 'a cancelled ticket is never refunded').",
55
+ ),
56
+ vague: q(
57
+ "Does `sentence` point at content instead of stating it: it announces, promises, sets up or labels a point that the sentence itself does not give?",
58
+ "A teaser, signpost or label: the reader learns that there is a reason, a tip, a rule or a next step, but not what it is.",
59
+ "The sentence gives the content itself, the paragraph gives it right after, or it starts with 'Link:' and names what the reader does when they click.",
60
+ ),
61
+ hedge: q(
62
+ "Does `sentence` pad itself with a hedge, a reassurance, a propping adverb (really, basically, actually), an unnamed source, or a line about the conversation instead of the subject?",
63
+ "It contains padding of that kind, or talks about the writing instead of the subject.",
64
+ "No hedge, prop or talk about the conversation. A thanks, a greeting or a question to the reader is not padding.",
65
+ ),
66
+ filler: q(
67
+ "Can one word or phrase in `sentence` be deleted, or swapped for a shorter one, and the sentence still say exactly the same thing?",
68
+ "At least one word can go or shrink ('in order to', 'each and every', a word the rest already implies) with no loss of meaning.",
69
+ "Every word carries meaning the sentence would lose. Warmth to the reader in a letter counts as meaning.",
70
+ ),
71
+ contrast: q(
72
+ "Does `sentence` stage a contrast to make its point: 'not X, it's Y' denying a claim nobody made, an either-or where one side is a strawman, a 'from X to Y' span with no real range, or one claim said twice in other words?",
73
+ "The denied half, the strawman side, the span or the repeat is there for effect. Delete it and the point stands alone.",
74
+ "A real comparison between two named options, or a single plain claim.",
75
+ ),
76
+ stilted: q(
77
+ "Does `sentence` sound awkward or unnatural, like a clumsy translation or a forced rewrite, so a native English speaker would phrase it differently or need to reread it?",
78
+ "Awkward phrasing: a person made the subject of find, meet or get where people would say 'there is' or 'X has'; an odd verb; an unexplained code or label; a clumsy word order.",
79
+ "Natural, clear English a person would say out loud.",
80
+ ),
81
+ pitch: q(
82
+ "Is `sentence` an ad line that promises an effortless result: a dramatic command plus an outcome said to be already true, or a lone word used as a verdict, instead of saying what happens when the reader takes an action?",
83
+ "Reads like a tagline: 'Do X, and Y is already done', or a one-word fragment as the punchline.",
84
+ `${PLAIN} Or a literal step with what happens next, in the product's own terms.`,
85
+ ),
86
+ personified: q(
87
+ "Does `sentence` use a figure where a literal word exists: a non-human thing doing what only a person does, or an abstract idea dressed as a physical object?",
88
+ "A tool that knows, wants, saves or gives; an idea that is a lever, a ceiling, a moat or a muscle.",
89
+ "The thing does something mechanical and literal, or a person does the acting.",
90
+ ),
91
+ thingdoer: q(
92
+ "Does `sentence` make a tool, automation, checklist, script or system the grammatical subject of what a person sets up or does, where a person as the subject ('we', 'you', or an imperative) would be plainer?",
93
+ "A noun leads and acts: 'An automation creates it and assigns it', 'A checklist catches this', 'The script only posts what is approved'. It reads like a description of a machine, with nobody behind it. Ramzi, 30 Sep 2026.",
94
+ "A person is the subject or the sentence is an instruction ('Have an automation create it'), or the thing genuinely is the subject of a literal fact ('The API returns JSON').",
95
+ ),
96
+ stagey: q(
97
+ "Does `sentence` describe an action or a rule as a small scene where a plain verb and object would do: 'waits for someone to say go' for 'needs your approval', 'gets you most of the way' for 'covers most of it', 'does its sweep' for 'runs'?",
98
+ "A phrase that acts out the idea (waits, says go, gets you there, takes the wheel) when one plain verb says the same thing. Ramzi, 30 Sep 2026: 'Claude Code on a timer requires you' is simpler and means the same.",
99
+ "The sentence already uses the plain verb and object a person would say out loud.",
100
+ ),
101
+ };
102
+
103
+ /** Asked once per paragraph of 3 or more sentences: the shapes no single sentence shows. */
104
+ export const PARAGRAPH_SHAPES = {
105
+ rhythm: q(
106
+ "Does `paragraph` repeat one structure instead of varying it: sentences of one length or one opening, items forced into threes, a punchy closer on every beat, a word reused where no new idea arrived, or a transition that asserts a link the facts do not show?",
107
+ "A reader could predict the shape of the next sentence before reading it.",
108
+ "Sentence lengths and openings vary with the thought, and each link between sentences is shown by what they say.",
109
+ ),
110
+ };
111
+
112
+ // =========================
113
+ // FUNCTIONS
114
+ // =========================
115
+ /** Short unpunctuated lines are glance copy (subjects, headlines); Ramzi, 23 Sep 2026, on a subject Jev never saw. */
116
+ const GLANCE = /^[^\n]{1,60}$/;
117
+
118
+ /** Prose sentences with their paragraph, plus glance lines of 4+ words as their own item. */
119
+ export function sentences(text) {
120
+ return text.split(/\n\s*\n|\n(?=\s*[-*]\s)/).flatMap((para) => {
121
+ const lines = para
122
+ .split("\n")
123
+ .map((l) => l.replace(/^\s*(?:#+|[-*])\s*/, "").trim())
124
+ .filter(Boolean);
125
+ if (lines.every((l) => GLANCE.test(l)) && lines.some((l) => !/[.!?]["')\]]*$/.test(l))) {
126
+ return lines
127
+ .filter((l) => l.split(" ").length >= 4)
128
+ .map((l) => ({ sentence: l, context: l }));
129
+ }
130
+ // An unpunctuated line is a heading: end it, so it isn't glued to the next sentence.
131
+ const flat = para.replace(/\s+/g, " ").trim();
132
+ const ended = lines.map((l) => (/[.!?:]["')\]]*$/.test(l) ? l : `${l}.`)).join(" ");
133
+ // A stop only ends a sentence before a space, so "1.5" stays whole.
134
+ return (ended.match(/.+?[.!?]+["')\]]*(?=\s|$)/g) ?? [])
135
+ .map((s) => s.trim())
136
+ .filter((s) => s.split(" ").length >= 3)
137
+ .map((sentence) => ({ sentence, context: flat }));
138
+ });
139
+ }
140
+
141
+ /** Markup as Jev reads it: a block ends a paragraph, and link text says it's a link. */
142
+ export function proseOf(html) {
143
+ return html
144
+ .replace(/<(script|style)[\s\S]*?<\/\1>|<!--[\s\S]*?-->/gi, " ")
145
+ .replace(/<a\b[^>]*>([\s\S]*?)<\/a>/gi, "\n\nLink: $1\n\n")
146
+ .replace(/<\/(p|li|h[1-6]|div|td|blockquote)>|<br\s*\/?>/gi, "\n\n")
147
+ .replace(/<[^>]+>/g, " ")
148
+ .replace(/&rarr;|&[a-z]+;|&#\d+;/gi, " ")
149
+ .replace(/[ \t]+/g, " ")
150
+ .replace(/\n\s*\n\s*/g, "\n\n")
151
+ .trim();
152
+ }
153
+
154
+ /** Paragraphs long enough to have a rhythm: 3 sentences or more. */
155
+ export function paragraphs(text) {
156
+ return text
157
+ .split(/\n\s*\n/)
158
+ .map((para) => para.replace(/\s+/g, " ").trim())
159
+ .filter((para) => (para.match(/[.!?]+["')\]]*(?=\s|$)/g) ?? []).length >= 3)
160
+ .map((paragraph) => ({ paragraph }));
161
+ }
162
+
163
+ /** `KILO_API_KEY` from the env, else Proton Pass (Dev vault, item Kilo), else "". */
164
+ export function kiloKey() {
165
+ if (process.env.KILO_API_KEY) return process.env.KILO_API_KEY;
166
+ try {
167
+ return execFileSync(
168
+ "pass-cli",
169
+ ["item", "view", "--vault-name", "Dev", "--item-title", "Kilo", "--field", "password"],
170
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 30_000 },
171
+ ).trim();
172
+ } catch {
173
+ return "";
174
+ }
175
+ }
176
+
177
+ /** The model check could not run: network, key, credits, timeout or a bad answer. Callers skip it, not crash. */
178
+ function unavailable(message) {
179
+ return Object.assign(new Error(message), { name: "JevUnavailable" });
180
+ }
181
+
182
+ export const isUnavailable = (err) => err instanceof Error && err.name === "JevUnavailable";
183
+
184
+ /** The URL is overridable so a test can point it at a local server or an unresolvable host. */
185
+ const endpoint = () => process.env.HEYRAMZI_JEV_URL || KILO_URL;
186
+
187
+ function why(err) {
188
+ if (err?.name === "TimeoutError") return `timed out after ${TIMEOUT_MS / 1000}s`;
189
+ return err?.cause?.code ?? err?.cause?.message ?? err?.message ?? String(err);
190
+ }
191
+
192
+ async function post(body, key, attempt = 0) {
193
+ let res;
194
+ try {
195
+ res = await fetch(endpoint(), {
196
+ method: "POST",
197
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
198
+ body: JSON.stringify(body),
199
+ signal: AbortSignal.timeout(TIMEOUT_MS),
200
+ });
201
+ } catch (err) {
202
+ throw unavailable(`${new URL(endpoint()).host} unreachable (${why(err)})`);
203
+ }
204
+ if ((res.status === 429 || res.status >= 500) && attempt < 4) {
205
+ await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
206
+ return post(body, key, attempt + 1);
207
+ }
208
+ if (!res.ok) throw unavailable(`Jev HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
209
+ const { answers } = await res.json().catch(() => ({}));
210
+ if (!answers) throw unavailable("Jev answered without scores");
211
+ return answers;
212
+ }
213
+
214
+ /** One request for every shape of up to BATCH items; item `i` is `i<n>` in state, so `field` becomes `i<n>.field`. */
215
+ async function askBatch(items, key, shapes, field) {
216
+ const state = Object.fromEntries(items.map((it, n) => [`i${n}`, it]));
217
+ const questions = {};
218
+ items.forEach((_, n) => {
219
+ for (const [id, q] of Object.entries(shapes)) {
220
+ questions[`i${n}_${id}`] = {
221
+ ...q,
222
+ instructions: q.instructions.replaceAll(`\`${field}\``, `\`i${n}.${field}\``),
223
+ };
224
+ }
225
+ });
226
+ const answers = await post({ model: MODEL, state, questions }, key);
227
+ return items.map((_, n) =>
228
+ Object.fromEntries(
229
+ Object.keys(shapes).map((id) => {
230
+ const score = answers[`i${n}_${id}`]?.noul;
231
+ if (typeof score !== "number") throw unavailable(`Jev left out the score for ${id}`);
232
+ return [id, score];
233
+ }),
234
+ ),
235
+ );
236
+ }
237
+
238
+ async function pooled(jobs) {
239
+ const out = [];
240
+ for (let i = 0; i < jobs.length; i += CONCURRENCY)
241
+ out.push(...(await Promise.all(jobs.slice(i, i + CONCURRENCY).map((job) => job()))));
242
+ return out;
243
+ }
244
+
245
+ // =========================
246
+ // CACHE
247
+ // =========================
248
+ /**
249
+ * Identical text scored 0.71, then 0.78 (12 Oct 2026). Worst-of-RUNS scores are kept per model, shape
250
+ * set and normalized text, so a re-run asks nothing.
251
+ */
252
+ const cacheRoot = () =>
253
+ process.env.HEYRAMZI_SLOP_CACHE ||
254
+ join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "heyramzi-slop");
255
+ /** `HEYRAMZI_SLOP_CACHE=off` asks the model every time, which is what a bench of the questions needs. */
256
+ const cacheOff = () => process.env.HEYRAMZI_SLOP_CACHE === "off";
257
+
258
+ const normalize = (text) => text.normalize("NFC").replace(/\s+/g, " ").trim();
259
+ const digest = (value) => createHash("sha256").update(value).digest("hex");
260
+
261
+ function cacheFile(item, field, shapesKey) {
262
+ const name = digest(JSON.stringify([MODEL, field, shapesKey, normalize(item[field])]));
263
+ return join(cacheRoot(), MODEL.replace(/[^\w.-]/g, "_"), `${name.slice(0, 40)}.json`);
264
+ }
265
+
266
+ function readScores(item, field, shapesKey, shapes) {
267
+ if (cacheOff()) return undefined;
268
+ try {
269
+ const scores = JSON.parse(readFileSync(cacheFile(item, field, shapesKey), "utf8"));
270
+ return Object.keys(shapes).every((id) => typeof scores[id] === "number") ? scores : undefined;
271
+ } catch {
272
+ // No entry, or a half-written one: ask the model.
273
+ return undefined;
274
+ }
275
+ }
276
+
277
+ function writeScores(item, field, shapesKey, scores) {
278
+ if (cacheOff()) return;
279
+ const file = cacheFile(item, field, shapesKey);
280
+ try {
281
+ mkdirSync(join(file, ".."), { recursive: true });
282
+ writeFileSync(`${file}.${process.pid}.tmp`, JSON.stringify(scores));
283
+ renameSync(`${file}.${process.pid}.tmp`, file);
284
+ } catch {
285
+ // An unwritable cache (a sandbox, a full disk) costs a re-ask, never the gate.
286
+ }
287
+ }
288
+
289
+ // =========================
290
+ // JUDGE
291
+ // =========================
292
+ /** Every item's worst score per shape over RUNS calls, kept from REVIEW up. Cached items cost no request. */
293
+ async function askAll(items, key, shapes, field) {
294
+ const shapesKey = digest(JSON.stringify(shapes));
295
+ const worst = items.map((it) => readScores(it, field, shapesKey, shapes));
296
+ const pending = items.flatMap((_, i) => (worst[i] ? [] : [i]));
297
+ const chunks = [];
298
+ for (let i = 0; i < pending.length; i += BATCH) chunks.push(pending.slice(i, i + BATCH));
299
+ const runs = await pooled(
300
+ chunks.flatMap((chunk) =>
301
+ Array.from({ length: RUNS }, () => async () => ({
302
+ chunk,
303
+ scores: await askBatch(
304
+ chunk.map((i) => items[i]),
305
+ key,
306
+ shapes,
307
+ field,
308
+ ),
309
+ })),
310
+ ),
311
+ );
312
+ for (const { chunk, scores } of runs) {
313
+ scores.forEach((byShape, n) => {
314
+ const seen = (worst[chunk[n]] ??= {});
315
+ for (const [id, p] of Object.entries(byShape)) seen[id] = Math.max(seen[id] ?? 0, p);
316
+ });
317
+ }
318
+ for (const i of pending) writeScores(items[i], field, shapesKey, worst[i]);
319
+ return items.flatMap((it, i) =>
320
+ Object.entries(worst[i])
321
+ .filter(([, p]) => p >= REVIEW)
322
+ .map(([id, p]) => ({ p, kind: "shape", id, detail: `${p.toFixed(2)} "${it[field]}"` })),
323
+ );
324
+ }
325
+
326
+ /** Every item asked every shape: `findings` at `block` and up (FAIL by default), `review` from REVIEW to `block`. */
327
+ export async function judge(items, shapes, field, key, block = FAIL) {
328
+ const hits = await askAll(items, key, shapes, field);
329
+ return { findings: hits.filter((h) => h.p >= block), review: hits.filter((h) => h.p < block) };
330
+ }
331
+
332
+ /**
333
+ * Shapes Jev reads in a text. A report (`review`) unless `block` names a bar; a hit at or above it
334
+ * is a `finding`. Throws JevUnavailable when the model can't answer.
335
+ */
336
+ export async function auditShapes(text, { key = process.env.KILO_API_KEY, block = Infinity } = {}) {
337
+ if (!key) throw unavailable(`--jev ${NO_KEY}`);
338
+ const a = await judge(sentences(text), SHAPES, "sentence", key, block);
339
+ const b = await judge(paragraphs(text), PARAGRAPH_SHAPES, "paragraph", key, block);
340
+ return { findings: [...a.findings, ...b.findings], review: [...a.review, ...b.review] };
341
+ }
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@heyramzi/lint",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.32.4",
4
+ "description": "Shared oxlint rules, base config and gates for Studio projects",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/heyramzi/vibe-kit.git",
9
+ "directory": "packages/lint"
10
+ },
11
+ "bin": {
12
+ "heyramzi-commit-msg": "./bin/heyramzi-commit-msg.js",
13
+ "heyramzi-comment-length": "./bin/heyramzi-comment-length.js",
14
+ "heyramzi-file-tokens": "./bin/heyramzi-file-tokens.js",
15
+ "heyramzi-inline-path": "./bin/heyramzi-inline-path.js",
16
+ "heyramzi-jev-code": "./bin/heyramzi-jev-code.js",
17
+ "heyramzi-push-diff": "./bin/heyramzi-push-diff.js",
18
+ "heyramzi-repo-rules": "./bin/heyramzi-repo-rules.js",
19
+ "heyramzi-slop": "./bin/heyramzi-slop.js",
20
+ "heyramzi-svelte-markup": "./bin/heyramzi-svelte-markup.js",
21
+ "heyramzi-ui-rules": "./bin/heyramzi-ui-rules.js"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "config",
26
+ "data",
27
+ "comment-length.js",
28
+ "commit-msg.js",
29
+ "file-tokens.js",
30
+ "gate-files.js",
31
+ "inline-path.js",
32
+ "index.js",
33
+ "jev.js",
34
+ "jev-code.js",
35
+ "package.json",
36
+ "processors",
37
+ "push-diff.js",
38
+ "slop.d.ts",
39
+ "slop.js",
40
+ "slop-views.js",
41
+ "ui-rules.js",
42
+ "svelte-markup.js",
43
+ "svelte-template.js",
44
+ "register.js",
45
+ "register.d.ts",
46
+ "repo-rules.js",
47
+ "repo-rules",
48
+ "scripts",
49
+ "rules"
50
+ ],
51
+ "type": "module",
52
+ "main": "index.js",
53
+ "imports": {
54
+ "#lint/*": "./*"
55
+ },
56
+ "exports": {
57
+ ".": "./index.js",
58
+ "./oxlint": "./config/oxlintrc.json",
59
+ "./slop-words": {
60
+ "types": "./data/slop-words.d.ts",
61
+ "default": "./data/slop-words.js"
62
+ },
63
+ "./register": {
64
+ "types": "./register.d.ts",
65
+ "default": "./register.js"
66
+ },
67
+ "./slop": {
68
+ "types": "./slop.d.ts",
69
+ "default": "./slop.js"
70
+ },
71
+ "./invisibles": "./data/invisibles.js"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ },
76
+ "dependencies": {
77
+ "@heyramzi/social": "^1.0.0"
78
+ },
79
+ "scripts": {
80
+ "voice:refresh": "node scripts/harvest-voice.mjs --write",
81
+ "test": "node --test \"*.test.mjs\""
82
+ }
83
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Processor that wraps each markdown line as a JS string literal,
3
+ * so stop-slop rules (no-ai-words, no-em-dash) can check prose.
4
+ */
5
+
6
+ export const markdownProcessor = {
7
+ meta: {
8
+ name: "stop-slop/markdown",
9
+ },
10
+
11
+ preprocess(text) {
12
+ const lines = text.split("\n");
13
+ const code = lines.map((line, i) => `const _line${i} = ${JSON.stringify(line)};`).join("\n");
14
+ return [{ text: code, filename: "0.js" }];
15
+ },
16
+
17
+ postprocess(messages) {
18
+ return messages.flat();
19
+ },
20
+
21
+ supportsAutofix: false,
22
+ };
package/push-diff.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The pushed-diff gate: a push may not change a persisted `StorageKey` string.
3
+ * Pure here, git and process in `bin/heyramzi-push-diff.js`.
4
+ */
5
+ import { commentSpans } from "#lint/comment-length.js";
6
+
7
+
8
+ /** The registry the add-ons persist under: `enum StorageKey {…}` or `const StorageKey = {…}`. */
9
+ const REGISTRY = /(?:\benum\s+StorageKey\s*\{|\bStorageKey\s*(?::[^=\n]+)?=\s*\{)/;
10
+
11
+ export const hasStorageKeys = (source) => REGISTRY.test(source);
12
+
13
+ /** Each `NAME: "value"` (or `NAME = "value"`) of the StorageKey registry. Comments are blanked first, so one holding a quote or a brace can't skew it. */
14
+ export function storageKeysOf(source) {
15
+ let clean = source;
16
+ for (const span of commentSpans(source).toReversed()) {
17
+ clean = clean.slice(0, span.start) + " ".repeat(span.end - span.start) + clean.slice(span.end);
18
+ }
19
+ const open = REGISTRY.exec(clean);
20
+ const keys = new Map();
21
+ if (!open) return keys;
22
+ let depth = 1;
23
+ let end = open.index + open[0].length;
24
+ while (end < clean.length && depth > 0) {
25
+ if (clean[end] === "{") depth += 1;
26
+ else if (clean[end] === "}") depth -= 1;
27
+ end += 1;
28
+ }
29
+ 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;
31
+ // A template that only splices string literals, `a_${"b"}`, is the same string at runtime.
32
+ const literal = (value) => value.replace(/\$\{\s*(["'])((?:(?!\1).)*)\1\s*\}/g, "$2");
33
+ for (const match of body.matchAll(entry)) keys.set(match[2], literal(match[4]));
34
+ return keys;
35
+ }
36
+
37
+ /** Keys present before and after whose string changed. Adding or deleting a key isn't a change of value. */
38
+ export function changedStorageValues(before, after) {
39
+ const was = storageKeysOf(before);
40
+ const now = storageKeysOf(after);
41
+ return [...was].flatMap(([name, value]) =>
42
+ now.has(name) && now.get(name) !== value ? [{ name, was: value, now: now.get(name) }] : [],
43
+ );
44
+ }
45
+
46
+ export function report(storage) {
47
+ const out = [];
48
+ for (const { file, name, was, now } of storage)
49
+ out.push(`${file}: StorageKey.${name} was "${was}" and is now "${now}"`);
50
+ if (storage.length > 0)
51
+ out.push(
52
+ "\nThose strings are live in users' Apps Script properties, and changing one orphans their data. Put each back to its old value; the name on the left is free to change.",
53
+ );
54
+ return out.join("\n");
55
+ }
package/register.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ export type RegisterName = "spoken" | "shorts" | "written";
2
+
3
+ export type RegisterMeasurement = {
4
+ words: number;
5
+ sentences: number;
6
+ meanSentence: number;
7
+ medianSentence: number;
8
+ /** Percent of sentences of 4 words or fewer. He does not fragment out loud. */
9
+ fragmentShare: number;
10
+ /** The same as a count. A single fragment never fails the band, however short the text. */
11
+ fragments: number;
12
+ /** Percent of sentences of 20 words or more. */
13
+ longShare: number;
14
+ /** Sentence-length spread, SD over mean. His floor is 0.34, machine prose sits near 0.1. */
15
+ lengthVariation: number;
16
+ contractions: number;
17
+ secondPerson: number;
18
+ firstPerson: number;
19
+ /** `and`, `so`, `because`, `but` per 1,000. He chains, he does not stack. */
20
+ chaining: number;
21
+ /** Second person for every first person. `Infinity` when there is no first person. */
22
+ egoRatio: number;
23
+ };
24
+
25
+ export type RegisterAudit = {
26
+ register: RegisterName;
27
+ label: string;
28
+ /** The corpus the band was counted from. */
29
+ source: string;
30
+ measured: RegisterMeasurement;
31
+ findings: { kind: "register"; id: string; detail: string }[];
32
+ };
33
+
34
+ /** Counts only, no judgement. The bands in `REGISTERS` are recounted from this. */
35
+ export declare function measureRegister(text: string): RegisterMeasurement;
36
+
37
+ export declare const REGISTERS: Record<
38
+ RegisterName,
39
+ { label: string; source: string; bands: Record<string, { min?: number; max?: number }> }
40
+ >;
41
+
42
+ /** Measure, then score against one band set. Findings merge into a prose audit. */
43
+ export declare function auditRegister(text: string, register?: RegisterName): RegisterAudit;