@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.
- package/LICENSE +21 -0
- package/README.md +277 -2
- package/bin/heyramzi-comment-length.js +99 -0
- package/bin/heyramzi-commit-msg.js +31 -0
- package/bin/heyramzi-file-tokens.js +66 -0
- package/bin/heyramzi-inline-path.js +79 -0
- package/bin/heyramzi-jev-code.js +107 -0
- package/bin/heyramzi-push-diff.js +93 -0
- package/bin/heyramzi-repo-rules.js +70 -0
- package/bin/heyramzi-slop.js +314 -0
- package/bin/heyramzi-svelte-markup.js +50 -0
- package/bin/heyramzi-ui-rules.js +56 -0
- package/comment-length.js +368 -0
- package/commit-msg.js +57 -0
- package/config/oxlintrc.json +87 -0
- package/data/invisibles.js +218 -0
- package/data/jev-bench.js +115 -0
- package/data/jev-code-bench.js +313 -0
- package/data/slop-words.d.ts +64 -0
- package/data/slop-words.js +2681 -0
- package/file-tokens.js +122 -0
- package/gate-files.js +14 -0
- package/index.js +116 -0
- package/inline-path.js +90 -0
- package/jev-code.js +194 -0
- package/jev.js +341 -0
- package/package.json +81 -4
- package/processors/markdown.js +22 -0
- package/push-diff.js +55 -0
- package/register.d.ts +43 -0
- package/register.js +255 -0
- package/repo-rules/packages.js +164 -0
- package/repo-rules/paths.js +101 -0
- package/repo-rules/toolchain.js +172 -0
- package/repo-rules/tree.js +51 -0
- package/repo-rules/workflows.js +116 -0
- package/repo-rules.js +54 -0
- package/rules/convex-ast.js +66 -0
- package/rules/convex-index-name.js +40 -0
- package/rules/convex-internal-only.js +59 -0
- package/rules/convex-no-date-now-in-query.js +48 -0
- package/rules/convex-no-deprecated-crons.js +40 -0
- package/rules/convex-no-filter.js +41 -0
- package/rules/convex-no-get-metadata.js +26 -0
- package/rules/convex-no-v-any.js +24 -0
- package/rules/convex-react-client-module-scope.js +37 -0
- package/rules/convex-require-args.js +39 -0
- package/rules/convex-require-returns.js +31 -0
- package/rules/convex-runtime-imports.js +48 -0
- package/rules/max-function-lines.js +105 -0
- package/rules/max-object-methods.js +86 -0
- package/rules/no-ai-words.js +78 -0
- package/rules/no-app-stores.js +25 -0
- package/rules/no-chained-type-assertions.js +91 -0
- package/rules/no-class.js +47 -0
- package/rules/no-conditional-empty-object-spread.js +64 -0
- package/rules/no-deep-phosphor-imports.js +29 -0
- package/rules/no-default-export.js +70 -0
- package/rules/no-em-dash.js +198 -0
- package/rules/no-emoji.js +56 -0
- package/rules/no-hardcoded-models.js +63 -0
- package/rules/no-inline-path.js +46 -0
- package/rules/no-inline-svg.js +67 -0
- package/rules/no-known-value-widening.js +289 -0
- package/rules/no-legacy-compat.js +35 -0
- package/rules/no-overkill-fallback.js +77 -0
- package/rules/no-raw-try-catch.js +26 -0
- package/rules/no-redundant-prop-fallback.js +153 -0
- package/rules/no-relative-imports.js +213 -0
- package/rules/no-relative-type-imports.js +40 -0
- package/rules/no-rune-outside-svelte-file.js +56 -0
- package/rules/no-rune-shadowing-binding.js +85 -0
- package/rules/no-skipped-or-focused-tests.js +74 -0
- package/rules/no-snake-case-props.js +45 -0
- package/rules/no-svelte4-syntax.js +33 -0
- package/rules/no-vitest-import-in-vite-plus.js +64 -0
- package/rules/props-interface.js +44 -0
- package/rules/redirect-external-needs-option.js +45 -0
- package/rules/require-safety-comment-for-type-assertion.js +110 -0
- package/rules/svelte-section-order.js +115 -0
- package/rules/test-file.js +8 -0
- package/scripts/count-hits.mjs +213 -0
- package/scripts/harvest-voice.mjs +161 -0
- package/scripts/jev-bench.mjs +41 -0
- package/scripts/jev-code-bench.mjs +61 -0
- package/scripts/measure-register.mjs +61 -0
- package/slop-views.js +122 -0
- package/slop.d.ts +53 -0
- package/slop.js +534 -0
- package/svelte-markup.js +107 -0
- package/svelte-template.js +258 -0
- package/ui-rules.js +324 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ramzi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,278 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @heyramzi/lint
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared lint rules for Studio projects: one oxlint plugin, the oxlint base config, and the gates a repo's pre-push hook runs.
|
|
4
|
+
|
|
5
|
+
## Included Tools
|
|
6
|
+
|
|
7
|
+
- **Oxlint plugin**: the rules below, loaded through `jsPlugins`.
|
|
8
|
+
- **Oxlint base config**: `@heyramzi/lint/oxlint`.
|
|
9
|
+
- **Gates**: `heyramzi-commit-msg`, `heyramzi-comment-length`, `heyramzi-file-tokens`, `heyramzi-inline-path`, `heyramzi-jev-code`, `heyramzi-repo-rules`, `heyramzi-slop` and `heyramzi-svelte-markup`.
|
|
10
|
+
|
|
11
|
+
There's no ESLint side. The Studio stack doesn't run ESLint, so the flat configs and the
|
|
12
|
+
`heyramzi-lint` pre-commit CLI are gone.
|
|
13
|
+
|
|
14
|
+
Formatting is not here. Vite+ reads oxfmt settings from the `fmt` block in a repo's
|
|
15
|
+
`vite.config.ts` and ignores `.oxfmtrc.json`, so a shared one was never read: it set tabs,
|
|
16
|
+
single quotes and no semicolons against a measured house style of the opposite, and it was
|
|
17
|
+
deleted on 14 Sep 2026.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
| Rule | Description |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `max-function-lines` | Enforces a maximum number of lines per function. |
|
|
24
|
+
| `no-ai-words` | Blocks common "AI slop" words (e.g., "delve", "embarking"). |
|
|
25
|
+
| `no-emoji` | Prevents the use of raw emojis in source code. `*.test.*`, `*.spec.*`, `__fixtures__/` and `fixtures/` files are exempt. Product code that writes emoji as data turns it off per file with an `overrides` entry (see [Config over comments](#config-over-comments)). |
|
|
26
|
+
| `no-hardcoded-models` | Error. A model id (`claude-…`, `gemini-…`, `gpt-…`, `grok-…`) in a string. Import `MODEL_IDS` or `MODEL_CATALOG` from `@heyramzi/ai`. Files named `ladder.ts`, test and fixture files, and `packages/ai` define or test the ids, so they pass. |
|
|
27
|
+
| `no-inline-svg` | Encourages the use of external SVG components or icons. |
|
|
28
|
+
| `no-inline-path` | Disallows inline SVG `<path>` elements in components. SVGs must be separate files and imported. |
|
|
29
|
+
| `no-raw-try-catch` | Requires the use of structured error handling helpers. |
|
|
30
|
+
| `svelte-section-order` | Enforces a specific order for Svelte 5 section comments. |
|
|
31
|
+
| `no-class` | Prevents the use of classes in favor of functional composition. |
|
|
32
|
+
| `no-default-export` | Enforces named exports for better discoverability and refactoring. |
|
|
33
|
+
| `no-relative-imports` | Error in every preset. Blocks relative specifiers in `import`, `export … from`, `import()`, `require` and `vi.mock`; only `./$types` is exempt. `--fix` rewrites each to the most specific alias covering the target: package.json `imports` first, then tsconfig `paths` for files that tsconfig includes. `.mjs`, `.cjs` and `*.config.*` files get `imports` only, because they run before a bundler applies aliases. A target no alias covers is reported with no fix: add the alias. |
|
|
34
|
+
| `no-relative-type-imports` | Deprecated and off: `no-relative-imports` covers type imports. |
|
|
35
|
+
| `no-em-dash` | Error. Em and en dashes in comments, strings, template text and Svelte text. A spaced hyphen (` - `) fails there too, except where it isn't a dash: a string whose whole value is one em or en dash (an empty-value placeholder), a string or template that is code judged on content (SQL keywords like `SELECT … FROM` or `WHERE`, CSS `calc(` or `var(--`, GLSL `gl_` or `vec2`, JS `function x(`, `=>` or `const x =`), a markdown bullet at the start of a template line or comment line, `<style>`, and `sql`/`css`/`gql`/`html`/`raw` templates. Test and fixture files (`*.test.*`, `*.spec.*`, `__fixtures__/`, `fixtures/`) pass a spaced hyphen because their strings are inputs under test. A real em or en dash fails everywhere except a bare placeholder and a tagged template. |
|
|
36
|
+
| `no-svelte4-syntax` | Error. `export let` and `$:` in a `.svelte` file. Svelte 4 markup is the markup gate's job. |
|
|
37
|
+
| `no-app-stores` | Error. An import from `$app/stores`; read `page` from `$app/state`. |
|
|
38
|
+
| `no-rune-outside-svelte-file` | Error. A rune in a file Svelte doesn't compile, which throws `rune_outside_svelte` at runtime. Rename it `.svelte.ts`. |
|
|
39
|
+
| `no-rune-shadowing-binding` | Error. A top-level binding named `state`, `derived`, `effect`… in a file that calls that rune, which throws `store_invalid_shape`. |
|
|
40
|
+
| `props-interface` | Error, with `--fix`. `type Props = {...}` in a component; write `interface Props`. A union, conditional or mapped type stays a type. |
|
|
41
|
+
| `redirect-external-needs-option` | Error. `redirect()` to a literal absolute URL without `{ external: true }`, a 500 in SvelteKit 3. |
|
|
42
|
+
| `no-redundant-prop-fallback` | Error. A `\|\|` or `??` fallback repeating the default a prop already has, in parameters, `$props()` and markup. |
|
|
43
|
+
| `no-vitest-import-in-vite-plus` | Error. An import from `vitest` in a Vite+ package with no `vitest` dependency of its own; import from `vite-plus/test`. |
|
|
44
|
+
| `no-skipped-or-focused-tests` | Error. `.only`, `.skip("title", fn)`, `xit`, `fit` and kin. A Playwright conditional skip, `test.skip(cond, "reason")`, passes. |
|
|
45
|
+
| `convex-*` | 10 rules, an `overrides` entry on `**/convex/**` in the base config. Error: `require-args` (and the old function syntax), `no-deprecated-crons`, `internal-only` (crons, scheduler, HTTP actions), `no-get-metadata`, `runtime-imports`. Warn while old hits remain: `require-returns`, `no-filter`, `no-v-any`, `no-date-now-in-query`, `index-name`. `convex-react-client-module-scope` is error everywhere. Replaces the `@convex-dev/eslint-plugin` the skill claimed and no repo installed. |
|
|
46
|
+
|
|
47
|
+
## The alias convention `no-relative-imports` enforces
|
|
48
|
+
|
|
49
|
+
Every repo in Studio now carries the same shape, applied on 17 Sep 2026 across 26 packages and
|
|
50
|
+
about 13,000 import lines:
|
|
51
|
+
|
|
52
|
+
- **Runtime aliases live in package.json `imports`**: `"#convex/*": "./convex/*"`. Node, tsx,
|
|
53
|
+
Vite, esbuild (so Convex), webpack (so Remotion) and WXT all resolve them, and a script run by
|
|
54
|
+
plain `node` resolves them too, which tsconfig `paths` never does.
|
|
55
|
+
- **TypeScript needs a mirror.** TypeScript 6 does not read `imports`: it matches the subpath and
|
|
56
|
+
then reports "does not exist in package.json scope", which surfaces as a wall of TS2307. Mirror
|
|
57
|
+
each entry into the `paths` of the tsconfig that covers those files.
|
|
58
|
+
- **In a SvelteKit app the mirror goes in `kit.alias`**, not in `tsconfig.json`. A `paths` block
|
|
59
|
+
in the app's own tsconfig replaces the generated one wholesale, which kills `$lib` and makes
|
|
60
|
+
`svelte-kit sync` warn; `kit.alias` is written into `.svelte-kit/tsconfig.json` instead.
|
|
61
|
+
- **A package that publishes `dist` needs a condition**, or the built file keeps a `#` specifier
|
|
62
|
+
that points at source the consumer cannot resolve:
|
|
63
|
+
`"#src/*": { "types": "./src/*", "default": "./dist/*" }`. An array of fallback targets does
|
|
64
|
+
not work: Node takes the first target and fails rather than trying the second.
|
|
65
|
+
- **A target outside the package** (a sibling directory, a file at the monorepo root) cannot be
|
|
66
|
+
an `imports` target, because those must start with `./`. Use `kit.alias` or tsconfig `paths`.
|
|
67
|
+
- The rule reads both sources and picks the deepest alias covering the target, so `$lib` wins
|
|
68
|
+
over `@/` inside `src/lib`. `.mjs`, `.cjs` and `*.config.*` files get `imports` only: they run
|
|
69
|
+
before a bundler applies an alias.
|
|
70
|
+
|
|
71
|
+
## Type-Evidence Rules
|
|
72
|
+
|
|
73
|
+
Ported from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (MIT). They reject
|
|
74
|
+
type claims with no evidence behind them.
|
|
75
|
+
|
|
76
|
+
| Rule | Preset | Description |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `no-chained-type-assertions` | `warn` | Blocks `x as unknown as T`. The top type launders the claim so the second assertion always compiles. `as const` chains are allowed. |
|
|
79
|
+
| `no-conditional-empty-object-spread` | `warn` | Blocks `...(cond ? { a } : {})` and `...(cond && { a })`. Build the object in statements and add the property only when it exists. |
|
|
80
|
+
| `no-known-value-widening` | `warn` | Blocks annotating a literal, object, array or function with `unknown`, `any`, `object` or `Record<string, unknown>`. Keep inference or use `satisfies`. Empty containers are exempt. |
|
|
81
|
+
| `require-safety-comment-for-type-assertion` | `off` | Requires a `SAFETY:` comment stating the invariant behind every non-const assertion. Off in the preset: the existing assertion backlog is large enough to bury every other rule. Turn it on per project when that project is ready to pay it down. |
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm add -D @heyramzi/lint
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
### OXLint Configuration
|
|
92
|
+
|
|
93
|
+
Extend the shared config in `.oxlintrc.json`:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"extends": ["@heyramzi/lint/oxlint"],
|
|
98
|
+
"jsPlugins": ["@heyramzi/lint"]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The `jsPlugins` entry activates the shared stop-slop rules (`no-em-dash`,
|
|
103
|
+
`no-ai-words`, `svelte-section-order`, etc.) inside oxlint.
|
|
104
|
+
|
|
105
|
+
### Ignores: each repo carries its own
|
|
106
|
+
|
|
107
|
+
oxlint resolves an extended config's `ignorePatterns` against that config's folder, which in a
|
|
108
|
+
consuming repo is `node_modules/@heyramzi/lint/config`, so they match nothing there. Checked on
|
|
109
|
+
the installed oxlint: a base `ignorePatterns` of `**/.agents/**` still lints `.agents/a.js`.
|
|
110
|
+
An `overrides` entry in the base does reach the repo, but it can only switch listed rules off,
|
|
111
|
+
and the correctness rules run from `categories`, so it can't stand in for an ignore.
|
|
112
|
+
|
|
113
|
+
Every repo therefore copies this block into its own `.oxlintrc.json` (the same list is
|
|
114
|
+
`ignorePatterns` in `config/oxlintrc.json`):
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
"ignorePatterns": [
|
|
118
|
+
"**/.agents/**", "**/.claude/**", "**/.opencode/**", "**/_generated/**",
|
|
119
|
+
"**/vendor/**", "**/.upstream/**", "**/*.generated.*", "**/public/**",
|
|
120
|
+
"**/node_modules/**", "**/dist/**", "build/**", "**/.svelte-kit/**", "**/.next/**",
|
|
121
|
+
"**/.output/**", "**/.vercel/**", "**/.wrangler/**", "**/coverage/**",
|
|
122
|
+
"**/scripts/secrets.mjs", "**/scripts/secrets-auto.mjs", "**/scripts/secrets.d.mts"
|
|
123
|
+
]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The last three are the shims `packages/secrets/src/wire.mjs` writes into each package.
|
|
127
|
+
`count-hits.mjs` applies the base list to every count, then names the repos that don't carry it.
|
|
128
|
+
|
|
129
|
+
### Config over comments
|
|
130
|
+
|
|
131
|
+
A file that has to break a rule on purpose gets an `overrides` entry in that repo's
|
|
132
|
+
`.oxlintrc.json`, never a disable comment. Product code that writes emoji as data (a parser
|
|
133
|
+
reading check-mark emoji, a template that seeds a Notion page) looks like this:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
"overrides": [
|
|
137
|
+
{ "files": ["**/project/emoji.ts", "**/clickup/clickup-task-fields.ts"],
|
|
138
|
+
"rules": { "@heyramzi/lint/no-emoji": "off" } }
|
|
139
|
+
]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Test and fixture files need no entry: `no-emoji` skips them already.
|
|
143
|
+
|
|
144
|
+
### Comment Length Gate
|
|
145
|
+
|
|
146
|
+
The conduct core caps a comment at 30 words. `heyramzi-comment-length` enforces it over a
|
|
147
|
+
whole repo: block comments, HTML comments and runs of consecutive line comments (`//`, `#`, `--`).
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pnpm exec heyramzi-comment-length # non-zero when any comment runs over the cap
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
It's a hard cap. There's no allowlist, no baseline and no `--update`: a file over the cap fails
|
|
154
|
+
until the comment is cut down, whether it's new or years old.
|
|
155
|
+
|
|
156
|
+
The same gate fails a comment that marks a hack left in place: a `TODO`, `FIXME` or `HACK` tag,
|
|
157
|
+
"for now", a temporary fix, backward compat, a legacy alias or path, or `@deprecated`. Agents
|
|
158
|
+
copy these along with the hack. Fix what the comment names, then delete it. A word inside
|
|
159
|
+
backticks is a quote and passes. The list is `MARKERS` in `comment-length.js`.
|
|
160
|
+
|
|
161
|
+
Every tracked `.ts .tsx .js .jsx .mjs .cjs .svelte .swift .py .sh .bash .zsh .css .sql` file is
|
|
162
|
+
read (the hack markers above apply to the first seven only), minus generated and vendored
|
|
163
|
+
trees (`dist`, `build`, `.svelte-kit`, `paraglide`, `vendor`, …) and symlinks. Narrow or widen
|
|
164
|
+
it with an optional `commentLength` key in the repo's root `package.json`:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"commentLength": {
|
|
169
|
+
"targets": ["app/src", "packages"],
|
|
170
|
+
"extensions": ["ts", "svelte"],
|
|
171
|
+
"ignore": ["_site"],
|
|
172
|
+
"max": 30
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The gate only bites when a hook runs it. `bash ai-doc/scripts/pre-push-hooks.sh [repo]`
|
|
178
|
+
(vibe-kit) writes a managed pre-push block into every Studio repo's active hook, whatever its
|
|
179
|
+
`core.hooksPath` (husky, Vite+ `.vite-hooks`, or `.git/hooks`); re-run it after a hook reset.
|
|
180
|
+
|
|
181
|
+
### Inline Path Gate
|
|
182
|
+
|
|
183
|
+
Components must not contain inline SVG `<path>` elements: vector graphics belong in separate `.svg` files and imported (e.g. `import icon from './icon.svg?raw'`), keeping component files concise. `heyramzi-inline-path` enforces it.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pnpm exec heyramzi-inline-path # non-zero when any component holds an inline path
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Like the comment length gate, it's a hard rule. There's no allowlist, no baseline and no
|
|
190
|
+
suppression comment: every inline `<path d="...">` fails until it moves to a `.svg` file.
|
|
191
|
+
|
|
192
|
+
Installed into every Studio repo's active pre-push hook by `bash ai-doc/scripts/pre-push-hooks.sh`.
|
|
193
|
+
|
|
194
|
+
### File Token Gate
|
|
195
|
+
|
|
196
|
+
No staged file may run past 5,000 tokens, counted as bytes / 4. `heyramzi-file-tokens` reads the
|
|
197
|
+
staged blobs, not the working copy, so it judges exactly what the commit records.
|
|
198
|
+
|
|
199
|
+
It skips what no person or agent writes by hand: lockfiles, `CHANGELOG.md`, `*.generated.*`,
|
|
200
|
+
minified bundles, source maps, snapshots, SVGs, binaries, generated and vendored trees (`dist`,
|
|
201
|
+
`_generated`, `vendor`, the `.claude/` projections, …), files whose first 2 KB carry a codegen
|
|
202
|
+
header (`@generated`, `Do not edit`, `Generated by`), and paths `.gitattributes` marks
|
|
203
|
+
`linguist-generated`. A pure rename passes. There's no config and no allowlist: a hand-written
|
|
204
|
+
file over the cap is split, and the refusal says how.
|
|
205
|
+
|
|
206
|
+
`bash ai-doc/scripts/pre-commit-hooks.sh [repo]` (vibe-kit) writes the managed pre-commit block
|
|
207
|
+
into every Studio repo's active hook, the same way `pre-push-hooks.sh` does for pre-push. It
|
|
208
|
+
replaced `eslint/max-lines` (600 code lines) and `CLIs/scripts/check-module-size.mjs`.
|
|
209
|
+
|
|
210
|
+
### UI Rules Gate
|
|
211
|
+
|
|
212
|
+
`heyramzi-ui-rules` judges staged files, or `PUSH_FILES` under the pre-push hook, and `--all` for the
|
|
213
|
+
whole tree. It fails on what the impeccable detector misses, and each failure says what to write instead:
|
|
214
|
+
`transition: all`, `scale(0)`, `ease-in`, literal or bare `duration-*`, padding on `.t-acc-panel`, `t-*`
|
|
215
|
+
classes in Swift, zoom-blocking viewports, `user-select: none` on body, `outline-slate|zinc|neutral`, raw
|
|
216
|
+
`:hover` outside `@media (hover: hover)`, a vendored shadcn kit, an `app.css` with no `@source` for
|
|
217
|
+
`@heyramzi/ui/src`, `adapter-cloudflare` without `paths.relative: false`, and a `static/` file under a route.
|
|
218
|
+
No allowlist and no config.
|
|
219
|
+
|
|
220
|
+
### Svelte Markup Gate
|
|
221
|
+
|
|
222
|
+
oxlint never reads a `.svelte` template, and skips a file with no `<script>`, so
|
|
223
|
+
`heyramzi-svelte-markup` reads the markup. It fails `<slot>` (outside a custom element),
|
|
224
|
+
`<svelte:component>`, an `on:` directive, a `class:` carrying a Tailwind arbitrary value (it never
|
|
225
|
+
applies; use `style:`), and a self-closing non-void HTML element.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
pnpm exec heyramzi-svelte-markup # the pushed .svelte files, or all of them outside a push
|
|
229
|
+
pnpm exec heyramzi-svelte-markup --all # every tracked .svelte file
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Generated and vendored trees are skipped (`DEFAULT_IGNORED` in `svelte-markup.js`). It runs in the
|
|
233
|
+
managed `push gates` block. No allowlist and no config.
|
|
234
|
+
|
|
235
|
+
### Repo Rules Gate
|
|
236
|
+
|
|
237
|
+
`heyramzi-repo-rules` judges staged files, and `--all` every tracked file for an audit. Each
|
|
238
|
+
failure says what to do instead. It reads the staged blob, so it judges what the commit records.
|
|
239
|
+
|
|
240
|
+
- **Package manager:** a staged `package-lock.json`, `yarn.lock`, `bun.lock` or `bun.lockb` fails.
|
|
241
|
+
- **@heyramzi dependencies:** a `link:`, `file:` or path spec fails, and so does any lockfile
|
|
242
|
+
entry with an `integrity` and no `tarball` URL.
|
|
243
|
+
- **Import maps:** a published `@heyramzi/*` package with `#src` imports maps `"#src/*.js"` to
|
|
244
|
+
`./src/*.ts` when it ships src, or `"#src/*"` to `{ types, default }` when it ships dist.
|
|
245
|
+
- **Credentials:** an `_authToken` line in a committed `.npmrc`, and the dead
|
|
246
|
+
`GITHUB_PRIVATE_PACKAGES_TOKEN` name in an install file.
|
|
247
|
+
- **Workflows:** a hosted runner, a write to `~/.npmrc`, a `pull_request` trigger, and any Node
|
|
248
|
+
that isn't 24.
|
|
249
|
+
- **Toolchain:** ESLint, knip, Prettier, Biome and Husky (dependency, script or config file),
|
|
250
|
+
hooks copied into `.git/hooks`, the fallow shape (dependency, `.fallowrc.json` schema first, 5
|
|
251
|
+
scripts, the `fallow` reference in `vibekit.json`, no tracked `.fallow/*.bin`), an
|
|
252
|
+
`.oxlintrc.json` that doesn't extend the shared base, and a `lint` block in `vite.config`.
|
|
253
|
+
- **Compose:** an `image:` that's `:latest` or untagged, unless the service builds it.
|
|
254
|
+
- **Fossil names:** an added path named `-old`, `-v2`, `-final` or `copy`, or under `misc/` or
|
|
255
|
+
`temp/`.
|
|
256
|
+
|
|
257
|
+
No allowlist, no config.
|
|
258
|
+
|
|
259
|
+
### Commit Message Gate
|
|
260
|
+
|
|
261
|
+
`heyramzi-commit-msg <file>` is a `commit-msg` hook. It refuses an em or en dash anywhere in the
|
|
262
|
+
message, in every repo. In a repo with a root `release-please-config.json` it also wants a
|
|
263
|
+
Conventional Commit subject (`type(scope): summary`), because release-please reads the type to
|
|
264
|
+
pick the version. Merge, revert, fixup and squash subjects pass.
|
|
265
|
+
|
|
266
|
+
The same repos refuse a `!` after the type and a `BREAKING CHANGE` footer. A breaking change is a
|
|
267
|
+
minor here, and release-please would cut a major from either one. A major only comes from a
|
|
268
|
+
`Release-As: X.0.0` footer written by hand. The `ui` repo has no release-please config, so the
|
|
269
|
+
gate also treats a root `scripts/release.sh` that mentions `Release-As` as one of those repos.
|
|
270
|
+
|
|
271
|
+
## Development
|
|
272
|
+
|
|
273
|
+
- `pnpm test`: runs `node --test` over the rule and gate tests.
|
|
274
|
+
- `node packages/lint/scripts/count-hits.mjs [repo ...]` (from vibe-kit): runs this working copy
|
|
275
|
+
over one Studio repo or all of them and prints error hits per rule per repo. `--warn` adds
|
|
276
|
+
warnings, `--rule <name>` filters, `--files` lists each hit, `--json` prints JSON. It writes a
|
|
277
|
+
temp config into each repo root for the run, because oxlint resolves `ignorePatterns` and plugin
|
|
278
|
+
paths against the config's folder, then deletes it.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The comment-length gate. Rule: `ai-doc/references/agents-core.md`. Usage and
|
|
4
|
+
* the `commentLength` config: this package's README.
|
|
5
|
+
*
|
|
6
|
+
* heyramzi-comment-length # non-zero when any comment runs over the cap
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { exit } from "node:process";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_EXTENSIONS,
|
|
16
|
+
DEFAULT_IGNORED,
|
|
17
|
+
MAX_WORDS,
|
|
18
|
+
check,
|
|
19
|
+
markerHits,
|
|
20
|
+
maxCommentWords,
|
|
21
|
+
selectFiles,
|
|
22
|
+
} from "#lint/comment-length.js";
|
|
23
|
+
import { gateFiles } from "#lint/gate-files.js";
|
|
24
|
+
|
|
25
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
26
|
+
const config = readConfig(repoRoot);
|
|
27
|
+
const max = config.max ?? MAX_WORDS;
|
|
28
|
+
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
const all = args.includes("--all");
|
|
31
|
+
const targets = args.filter((arg) => !arg.startsWith("-"));
|
|
32
|
+
if (all) delete process.env.PUSH_FILES;
|
|
33
|
+
|
|
34
|
+
const ignored = [...DEFAULT_IGNORED, ...(config.ignore ?? [])];
|
|
35
|
+
const candidates =
|
|
36
|
+
targets.length > 0
|
|
37
|
+
? targets
|
|
38
|
+
: process.env.PUSH_FILES !== undefined
|
|
39
|
+
? process.env.PUSH_FILES.split("\n").filter(Boolean)
|
|
40
|
+
: tracked(repoRoot);
|
|
41
|
+
const files = gateFiles(repoRoot, selectFiles(candidates, { ...config, ignored }));
|
|
42
|
+
|
|
43
|
+
gate(files);
|
|
44
|
+
|
|
45
|
+
function readConfig(root) {
|
|
46
|
+
const manifest = path.join(root, "package.json");
|
|
47
|
+
if (!existsSync(manifest)) return {};
|
|
48
|
+
return JSON.parse(readFileSync(manifest, "utf8")).commentLength ?? {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function tracked(root) {
|
|
52
|
+
const patterns = (config.extensions ?? DEFAULT_EXTENSIONS).map(
|
|
53
|
+
(ext) => `*.${ext.replace(/^\./, "")}`,
|
|
54
|
+
);
|
|
55
|
+
return execFileSync("git", ["ls-files", ...patterns], { cwd: root, encoding: "utf8" })
|
|
56
|
+
.trim()
|
|
57
|
+
.split("\n")
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extOf(file) {
|
|
62
|
+
return file.slice(file.lastIndexOf(".") + 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function measure(files) {
|
|
66
|
+
const lengths = {};
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
lengths[file] = maxCommentWords(readFileSync(path.join(repoRoot, file), "utf8"), extOf(file));
|
|
69
|
+
}
|
|
70
|
+
return lengths;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function gate(files) {
|
|
74
|
+
const failures = check(measure(files), max);
|
|
75
|
+
const markers = files.flatMap((file) =>
|
|
76
|
+
markerHits(readFileSync(path.join(repoRoot, file), "utf8"), extOf(file)).map((hit) => ({
|
|
77
|
+
file,
|
|
78
|
+
...hit,
|
|
79
|
+
})),
|
|
80
|
+
);
|
|
81
|
+
if (failures.length === 0 && markers.length === 0) {
|
|
82
|
+
console.log(`comment length: every comment is ${max} words or fewer, with no hack markers`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const { file, words } of failures) {
|
|
87
|
+
console.error(`${file}: longest comment ${words} words (over ${max})`);
|
|
88
|
+
}
|
|
89
|
+
if (failures.length > 0) console.error("\nCompress the comment to the point it makes.");
|
|
90
|
+
for (const { file, line, label } of markers) {
|
|
91
|
+
console.error(`${file}:${line}: ${label}`);
|
|
92
|
+
}
|
|
93
|
+
if (markers.length > 0) {
|
|
94
|
+
console.error(
|
|
95
|
+
"\nFix what the comment names, or delete it. No legacy name survives a refactor.",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
exit(1);
|
|
99
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The commit-msg gate. Usage: this package's README, "Commit Message Gate".
|
|
4
|
+
*
|
|
5
|
+
* heyramzi-commit-msg .git/COMMIT_EDITMSG # non-zero when the message breaks a rule
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { argv, exit } from "node:process";
|
|
12
|
+
|
|
13
|
+
import { check, report } from "#lint/commit-msg.js";
|
|
14
|
+
|
|
15
|
+
const messageFile = argv[2];
|
|
16
|
+
if (!messageFile) {
|
|
17
|
+
console.error("commit-msg: pass the message file git hands the hook");
|
|
18
|
+
exit(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
22
|
+
const releaseScript = path.join(repoRoot, "scripts/release.sh");
|
|
23
|
+
const conventional =
|
|
24
|
+
existsSync(path.join(repoRoot, "release-please-config.json")) ||
|
|
25
|
+
(existsSync(releaseScript) && readFileSync(releaseScript, "utf8").includes("Release-As"));
|
|
26
|
+
const problems = check(readFileSync(messageFile, "utf8"), { conventional });
|
|
27
|
+
|
|
28
|
+
if (problems.length > 0) {
|
|
29
|
+
console.error(report(problems));
|
|
30
|
+
exit(1);
|
|
31
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The file-token gate over staged files. Usage: this package's README, "File Token Gate".
|
|
4
|
+
*
|
|
5
|
+
* heyramzi-file-tokens # non-zero when a staged file runs over 5,000 tokens
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { exit } from "node:process";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
MAX_TOKENS,
|
|
13
|
+
check,
|
|
14
|
+
generatedContent,
|
|
15
|
+
ignoredPath,
|
|
16
|
+
report,
|
|
17
|
+
} from "#lint/file-tokens.js";
|
|
18
|
+
|
|
19
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
20
|
+
const git = (args, input) =>
|
|
21
|
+
execFileSync("git", args, { cwd: repoRoot, input, maxBuffer: 256 * 1024 * 1024 });
|
|
22
|
+
|
|
23
|
+
const failures = check(stagedSizes(staged().filter((file) => !ignoredPath(file))));
|
|
24
|
+
const real = failures.filter(({ file }) => !generated(file));
|
|
25
|
+
|
|
26
|
+
if (real.length === 0) {
|
|
27
|
+
console.log(`file-tokens: every staged file is under ${MAX_TOKENS.toLocaleString("en-US")} tokens`);
|
|
28
|
+
} else {
|
|
29
|
+
console.error(report(real));
|
|
30
|
+
exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Added, copied, modified and renamed files. A pure move (R100) changes nothing, so it passes. */
|
|
34
|
+
function staged() {
|
|
35
|
+
const fields = git(["diff", "--cached", "--name-status", "-M", "-z", "--diff-filter=ACMR"])
|
|
36
|
+
.toString("utf8")
|
|
37
|
+
.split("\0");
|
|
38
|
+
const files = [];
|
|
39
|
+
for (let index = 0; index < fields.length - 1; ) {
|
|
40
|
+
const status = fields[index];
|
|
41
|
+
const paired = status.startsWith("R") || status.startsWith("C");
|
|
42
|
+
const target = fields[index + (paired ? 2 : 1)];
|
|
43
|
+
if (status !== "R100") files.push(target);
|
|
44
|
+
index += paired ? 3 : 2;
|
|
45
|
+
}
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Byte size of each file's staged blob, which is what gets committed, not the working copy. */
|
|
50
|
+
function stagedSizes(files) {
|
|
51
|
+
if (files.length === 0) return {};
|
|
52
|
+
const lines = git(["cat-file", "--batch-check=%(objectsize)"], files.map((file) => `:${file}`).join("\n"))
|
|
53
|
+
.toString("utf8")
|
|
54
|
+
.trim()
|
|
55
|
+
.split("\n");
|
|
56
|
+
return Object.fromEntries(
|
|
57
|
+
files.flatMap((file, index) => (/^\d+$/.test(lines[index]) ? [[file, Number(lines[index])]] : [])),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Content markers or `.gitattributes` say a machine wrote this file. Checked only for files over the cap. */
|
|
62
|
+
function generated(file) {
|
|
63
|
+
const attribute = git(["check-attr", "--cached", "linguist-generated", "--", file]).toString("utf8");
|
|
64
|
+
if (/: (set|true)\s*$/.test(attribute)) return true;
|
|
65
|
+
return generatedContent(git(["cat-file", "blob", `:${file}`]).subarray(0, 8000));
|
|
66
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The inline-path gate. Disallows inline SVG <path> elements in components.
|
|
4
|
+
* SVGs must be separate files (.svg) and imported. Usage in packages/lint/README.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { exit } from "node:process";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_EXTENSIONS,
|
|
14
|
+
DEFAULT_IGNORED,
|
|
15
|
+
check,
|
|
16
|
+
countInlinePaths,
|
|
17
|
+
findInlinePaths,
|
|
18
|
+
selectFiles,
|
|
19
|
+
} from "#lint/inline-path.js";
|
|
20
|
+
import { gateFiles } from "#lint/gate-files.js";
|
|
21
|
+
|
|
22
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
23
|
+
const config = readConfig(repoRoot);
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const all = args.includes("--all");
|
|
27
|
+
const targets = args.filter((arg) => !arg.startsWith("-"));
|
|
28
|
+
if (all) delete process.env.PUSH_FILES;
|
|
29
|
+
|
|
30
|
+
const ignored = [...DEFAULT_IGNORED, ...(config.ignore ?? [])];
|
|
31
|
+
const files = gateFiles(repoRoot, selectFiles(tracked(repoRoot), { ...config, ignored, targets }));
|
|
32
|
+
|
|
33
|
+
gate(files);
|
|
34
|
+
|
|
35
|
+
function readConfig(root) {
|
|
36
|
+
const manifest = path.join(root, "package.json");
|
|
37
|
+
if (!existsSync(manifest)) return {};
|
|
38
|
+
return JSON.parse(readFileSync(manifest, "utf8")).inlinePath ?? {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tracked(root) {
|
|
42
|
+
const extensions = config.extensions ?? DEFAULT_EXTENSIONS;
|
|
43
|
+
const patterns = extensions.map((ext) => `*.${ext.replace(/^\./, "")}`);
|
|
44
|
+
return execFileSync(
|
|
45
|
+
"git",
|
|
46
|
+
["ls-files", "--cached", "--others", "--exclude-standard", ...patterns],
|
|
47
|
+
{ cwd: root, encoding: "utf8" },
|
|
48
|
+
)
|
|
49
|
+
.trim()
|
|
50
|
+
.split("\n")
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function measure(files) {
|
|
55
|
+
const counts = {};
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
counts[file] = countInlinePaths(readFileSync(path.join(repoRoot, file), "utf8"));
|
|
58
|
+
}
|
|
59
|
+
return counts;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function gate(files) {
|
|
63
|
+
const failures = check(measure(files));
|
|
64
|
+
if (failures.length === 0) {
|
|
65
|
+
console.log("inline-path: no inline SVG paths");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const { file, count } of failures) {
|
|
70
|
+
const details = findInlinePaths(readFileSync(path.join(repoRoot, file), "utf8"))
|
|
71
|
+
.map((m) => ` line ${m.line}: ${m.snippet}`)
|
|
72
|
+
.join("\n");
|
|
73
|
+
console.error(`${file}: ${count} inline path(s):\n${details}`);
|
|
74
|
+
}
|
|
75
|
+
console.error(
|
|
76
|
+
"\nSVGs must be separate files (.svg) and imported. Extract inline paths to separate .svg files.",
|
|
77
|
+
);
|
|
78
|
+
exit(1);
|
|
79
|
+
}
|