eslint-plugin-legibility
v0.4.0
Published
ESLint and Oxlint JS plugin rules for readable, performance-conscious code.
Maintainers
Readme
eslint plugin legibility
Why was this written?
Working with LLMs for the majority of my work, I find the way that I code and read code has changed. This project contains rules I find useful for keeping TypeScript and/or JavaScript more readable when written mainly by LLMs.
TLDR;
The goal of rules in this package are to make code readable for reviewing lots of code and avoiding things that have a high probability of complexity or confusion.
Install
This project provides ESLint and Oxlint-compatible rules for readable, explicit, performance-conscious JavaScript and TypeScript.
The package exports an ESLint-compatible root plugin and an Oxlint-focused eslint-plugin-legibility/oxlint entry. ESLint loads the root package directly. Oxlint loads the Oxlint entry through JavaScript plugin support.
# npm, pnpm, bun
npm add -D eslint-plugin-legibilityConfigs
Choose the preset for your linter and severity.
flat/recommended
Enables the broadly applicable legibility rules and core complexity limits as warnings.
import legibility from "eslint-plugin-legibility";
export default [legibility.configs["flat/recommended"]];flat/strict
Enables every recommended rule plus the more opinionated analysis rules as errors.
import legibility from "eslint-plugin-legibility";
export default [legibility.configs["flat/strict"]];flat/agent-recommended
Mirrors flat/recommended, but requires named values before object construction and returns.
import legibility from "eslint-plugin-legibility";
export default [legibility.configs["flat/agent-recommended"]];flat/agent-strict
Mirrors flat/strict, but requires named values before object construction and returns.
import legibility from "eslint-plugin-legibility";
export default [legibility.configs["flat/agent-strict"]];oxlint.configs.recommended
Mirrors flat/recommended in oxlint.config.ts.
import { defineConfig } from "oxlint";
import legibility from "eslint-plugin-legibility/oxlint";
export default defineConfig(legibility.configs.recommended);oxlint.configs.strict
Mirrors flat/strict in oxlint.config.ts.
import { defineConfig } from "oxlint";
import legibility from "eslint-plugin-legibility/oxlint";
export default defineConfig(legibility.configs.strict);oxlint.configs.agentRecommended
Mirrors flat/agent-recommended in oxlint.config.ts.
import { defineConfig } from "oxlint";
import legibility from "eslint-plugin-legibility/oxlint";
export default defineConfig(legibility.configs.agentRecommended);oxlint.configs.agentStrict
Mirrors flat/agent-strict in oxlint.config.ts.
import { defineConfig } from "oxlint";
import legibility from "eslint-plugin-legibility/oxlint";
export default defineConfig(legibility.configs.agentStrict);All presets explicitly configure these core rules because ESLint's recommended config does not enable them:
complexity: maximum cyclomatic complexity of20.max-lines-per-function:max: 40, excluding blank lines and comments and including IIFEs.
Rules
recommended contains broadly applicable legibility checks. strict includes every recommended rule plus more opinionated performance and code-shape analysis. agent-recommended and agent-strict keep the same rule membership as their base presets, but make computed object and return values stricter for agent-authored code. Composition style, executable-entry checks, filename schemas, and blanket comment policies remain opt-in because they require a project decision.
legibility/hoist-if-operatorslegibility/max-array-chain-depthlegibility/max-control-flow-depthlegibility/max-expression-operatorslegibility/max-function-parameterslegibility/no-complex-ternarieslegibility/no-computed-valueslegibility/no-hidden-side-effectslegibility/no-identity-array-callbacklegibility/no-mixed-filename-casinglegibility/no-automated-comment-attributionlegibility/no-redundant-boolean-logiclegibility/no-redundant-nullish-fallbacklegibility/no-stacked-commentslegibility/no-trivial-wrapper-functionslegibility/no-unnecessary-block-callbacklegibility/prefer-early-returnlegibility/prefer-flat-maplegibility/prefer-guard-clauseslegibility/prefer-object-lookuplegibility/prefer-positive-condition-nameslegibility/require-jsdoc-multiline-comments
legibility/no-direct-node-bin-smokelegibility/no-quadratic-patternslegibility/no-repeated-collection-searchlegibility/no-single-use-renaming-aliaslegibility/no-small-collection-conversionlegibility/no-standalone-array-mutationslegibility/no-unnecessary-async
legibility/no-unmatched-commentslegibility/prefer-concat-object-assignlegibility/require-executable-shebanglegibility/require-filename-matches-dirname
legibility/hoist-if-operators({options})
Prefer a named boolean before an operator-heavy if condition.
options
{max: number}: allowed weighted operators in the condition. Default:0.{operators: string[]}: operators to count. Default:["&&", "||", "??", "?:"].{complexity: Record<string, number>}: per-operator weights.
do / don't
- if (user && user.isActive && !user.isLocked) {
- sendInvite(user);
- }
+ const canInviteUser = user && user.isActive && !user.isLocked;
+
+ if (canInviteUser) {
+ sendInvite(user);
+ }legibility/max-array-chain-depth({options})
Limit chained array methods like items.filter().map().some().
options
{max: number}: allowed chained items. Default:2.{iterationMethods: string[]}: method names that count as chain items.
do / don't
- const hasLargeActiveItem = items
- .filter((item) => item.active)
- .map((item) => item.size)
- .some((size) => size > 100);
+ const activeItems = items.filter((item) => item.active);
+ const itemSizes = activeItems.map((item) => item.size);
+ const hasLargeActiveItem = itemSizes.some((size) => size > 100);legibility/max-control-flow-depth({options})
Limit nested branches and loops.
options
{max: number}: allowed nested control-flow depth. Default:3.
do / don't
- if (user) {
- if (user.active) {
- if (user.email) {
- sendInvite(user);
- }
- }
- }
+ if (!user) return;
+ if (!user.active) return;
+ if (!user.email) return;
+
+ sendInvite(user);legibility/max-expression-operators({options})
Limit operators inside one expression.
options
{max: number}: allowed weighted operators. Default:4.{operators: string[]}: operators to count.{complexity: Record<string, number>}: per-operator weights.
do / don't
- return user && user.active && (user.role === "admin" || user.role === "owner");
+ const isAdmin = user.role === "admin";
+ const isOwner = user.role === "owner";
+ const hasPrivilegedRole = isAdmin || isOwner;
+
+ return user && user.active && hasPrivilegedRole;legibility/max-function-parameters({options})
Limit the inputs a function exposes. The rule checks both top-level parameters and the properties listed by each destructured object parameter.
TypeScript ambient declarations and function types are checked. A leading TypeScript this parameter is ignored because callers do not supply it.
options
{max: number}: allowed top-level parameters. Default:4.{maxObjectProperties: number}: allowed properties in one destructured object parameter. Default:8.
do / don't
- function schedule(user, plan, timezone, locale, notify) {}
+ function schedule(request, deliveryOptions) {}
- function publish({ article, author, channel, locale, schedule, tags, theme, tracking, visibility }) {}
+ function publish(article, publicationOptions) {}legibility/no-complex-ternaries({options})
Reject nested ternaries and operator-heavy ternaries.
options
{max: number}: allowed weighted operators inside one ternary. Default:2.{operators: string[]}: operators to count.{complexity: Record<string, number>}: per-operator weights.
do / don't
- const label = isLoading ? "Loading" : hasError ? "Error" : "Ready";
+ const label = getStatusLabel({ hasError, isLoading });legibility/no-computed-values({options})
Prefer named values before computed returns and object values.
options
{max: number}: allowed weighted operators in a computed value. Default:1.{operators: string[]}: operators to count.{complexity: Record<string, number>}: per-operator weights.{objectValues: "computed" | "named"}: object value mode. Default:"computed".{returnValues: "computed" | "named"}: return value mode. Default:"computed".
Use "named" mode to require a named identifier or literal before object construction and returns.
do / don't
- return subtotal + tax - discount;
+ const total = subtotal + tax - discount;
+
+ return total;
- return { route: getRouteName(url.pathname) };
+ const route = getRouteName(url.pathname);
+
+ return route;legibility/no-direct-node-bin-smoke({options})
Smoke-test installed package bins instead of direct node src/index.js execution.
options
{entryPatterns: string[]}: entry files that should be tested through the installed bin shim.
do / don't
- execSync("node src/index.js --help");
+ execSync("my-cli --help");legibility/no-hidden-side-effects({options})
Keep mutations out of nested expressions and side-effect-free callbacks.
options
{mutatingMethods: string[]}: method calls treated as mutations.{sideEffectFreeIterationMethods: string[]}: callback methods expected to stay side-effect-free.
do / don't
- return (count += 1);
+ count += 1;
+
+ return count;legibility/no-identity-array-callback()
Reject map and filter callbacks that keep every item unchanged.
do / don't
- const nextItems = items.map((item) => item);
+ const nextItems = items;legibility/no-mixed-filename-casing()
Use one filename convention: kebab-case, camelCase, PascalCase, or snake_case. Leading dots and file extensions are ignored.
This rule has no options. It rejects conventions mixed within one filename; it does not require every file in the project to use the same convention. For example, user-profile.ts, userProfile.ts, UserProfile.ts, and user_profile.ts are all valid.
do / don't
- my-File.ts
+ my-file.ts
- user_profile-card.test.ts
+ user_profile_card.test.ts
- accountSettings-helper.ts
+ account-settings-helper.tslegibility/no-quadratic-patterns({options})
Flag nested loops, nested array iteration, and collection searches inside loop bodies.
options
{iterationMethods: string[]}: methods checked for nested iteration.{searchMethods: string[]}: methods treated as collection searches.
do / don't
- const enrichedOrders = orders.map((order) => ({
- ...order,
- user: users.find((user) => user.id === order.userId),
- }));
+ const usersById = new Map(users.map((user) => [user.id, user]));
+ const enrichedOrders = orders.map((order) => ({
+ ...order,
+ user: usersById.get(order.userId),
+ }));legibility/no-redundant-boolean-logic({options})
Avoid boolean comparisons and boolean-only ternaries.
options
{equalityOperators: string[]}: operators checked for comparisons againsttrueorfalse. Default:["==", "===", "!=", "!=="].
do / don't
- return isReady === true ? true : false;
+ return isReady;legibility/no-redundant-nullish-fallback()
Avoid ?? undefined fallbacks.
Static void operands are evaluated within a bounded BigInt budget. Expressions that could
create unusually large BigInt values are ignored.
do / don't
- const value = maybeValue ?? undefined;
+ const value = maybeValue;legibility/no-repeated-collection-search({options})
Flag repeated searches over the same collection in one scope.
options
{searchMethods: string[]}: methods treated as collection searches.
do / don't
- const owner = users.find((user) => user.id === ownerId);
- const reviewer = users.find((user) => user.id === reviewerId);
+ const usersById = new Map(users.map((user) => [user.id, user]));
+ const owner = usersById.get(ownerId);
+ const reviewer = usersById.get(reviewerId);legibility/no-small-collection-conversion({options})
Avoid converting a statically small array or string into a Map or Set for one immediate lookup. Named collections, dynamic inputs, and literal inputs at the threshold are unchanged.
options
{min: number}: minimum known input size before a lookup collection is useful. Default:3.
do / don't
- const isTerminal = new Set(["done", "failed"]).has(status);
+ const isTerminal = ["done", "failed"].includes(status);legibility/no-single-use-renaming-alias()
Avoid aliases that only rename another value for one use.
do / don't
- const userData = user;
-
- return userData.name;
+ return user.name;legibility/no-standalone-array-mutations({options})
Prefer explicit returned array composition over standalone array mutation statements.
options
{arrayMutatingMethods: string[]}: array methods reported when used as standalone mutations.{mutatingMethods: string[]}: mutation methods used to identify fresh mutation targets.
do / don't
- items.push(nextItem);
-
- return items;
+ return items.concat(nextItem);legibility/no-trivial-wrapper-functions()
Avoid wrappers that only forward their parameters to another call.
do / don't
- const getUser = (userId) => fetchUser(userId);
+ const getActiveUser = (userId) => fetchUser(userId).then(requireActiveUser);legibility/no-unmatched-comments({options})
Allow only comments that match an explicit pattern, prefix, or suffix. With no options, the rule rejects every line and block comment. Executable shebangs are ignored.
options
matchers: case-insensitive regular expressions matched against the comment body.prefixIdentifiers: case-insensitive identifiers allowed at the start of a comment.suffixIdentifiers: case-insensitive identifiers allowed at the end of a comment.
All options accept string arrays and default to empty arrays.
The three options are independent allow paths. Matching ignores comment delimiters, surrounding whitespace, and leading JSDoc stars. Invalid regular expressions and empty identifiers never match. The rule has no autofix.
do / don't
With the WHY: prefix configured:
- // Retry after the provider resets its rate limit.
+ // WHY: The provider resets its rate limit every 30 seconds.legibility/no-stacked-comments()
Reject comments on consecutive lines. A blank line between comments is allowed.
do / don't
- // Retry every failed request.
- // Retry requests that fail during regional failover.
+ // Retry only requests that fail during regional failover.The rule has no options and no autofix.
legibility/no-automated-comment-attribution({options})
Reject comments that explicitly attribute authorship to an automated tool. The rule detects configured identifiers in attribution tags, generated by <identifier>, and <identifier>-generated. It does not classify unmarked prose.
options
{identifiers: string[]}: case-insensitive names treated as automated sources. Default:ai,chatgpt,claude,codex,copilot,gemini,gpt,llm, andopenai.
do / don't
- // Generated by Codex. Normalize provider errors before retrying.
+ // Normalize provider errors before retrying.legibility/require-jsdoc-multiline-comments()
Require block comments spanning multiple lines to use /** ... */ JSDoc syntax. Line comments and single-line block comments are unchanged.
This formatting rule does not permit agents to add comments. During an agent session, npx lint-changed --comments=forbid rejects every added comment, including separated comments. Outside that session policy, ESLint and Oxlint can autofix this rule by adding the missing * to the block opener.
do / don't
- /*
- * The provider can return a stale token during regional failover.
- * Preserve the retry order.
- */
+ /**
+ * The provider can return a stale token during regional failover.
+ * Preserve the retry order.
+ */This rule has no options. Run ESLint or Oxlint with --fix to apply the autofix.
legibility/no-unnecessary-async()
Flag async functions that have no await, only return one awaited value, or only await Node filesystem operations with synchronous equivalents. Filesystem detection covers named and namespace imports from node:fs/promises, fs/promises, node:fs, and fs.
Use the filesystem diagnostic for local tooling and scripts. Non-blocking filesystem I/O remains appropriate in request-serving code.
do / don't
- import { readFile } from "node:fs/promises";
+ import { readFileSync } from "node:fs";
- async function readConfig() {
- return await readFile("config.json", "utf8");
+ function readConfig() {
+ return readFileSync("config.json", "utf8");
}legibility/no-unnecessary-block-callback()
Prefer expression-bodied arrow callbacks when the callback block only returns.
do / don't
- const ids = users.map((user) => {
- return user.id;
- });
+ const ids = users.map((user) => user.id);legibility/prefer-concat-object-assign()
Report array and object literals containing spread when a project prefers method-based composition:
- Array literal spread is reported in favor of
Array#concat. - Object literal spread is reported in favor of
Object.assignwith a new target. - Function-call spread and rest syntax are unchanged.
This rule has no options or autofix. Enable it explicitly:
import legibility from "eslint-plugin-legibility";
+const compositionRules = {
+ "legibility/prefer-concat-object-assign": "warn",
+};
+const compositionConfig = { rules: compositionRules };
+
export default [
legibility.configs["flat/recommended"],
+ compositionConfig,
];why it is opt-in
This is a style opinion, not a universal performance rule. concat names the array composition operation. Object.assign names the object composition operation, makes the fresh target visible, and preserves source precedence in argument order.
ESLint's opposing prefer-object-spread rule says object spread may perform better. V8's spread documentation describes a fast path when spread begins an array literal, including [...items, nextItem], but not when values precede it, as in [firstItem, ...items]. Engine, placement, collection size, and data shape can change the result, so neither form is always faster.
The forms can also behave differently. Object.assign uses assignment semantics, while object spread creates data properties. concat observes concat-spreadability, while array spread uses iteration. The rule therefore reports the syntax but leaves the change to the developer.
do / don't
For ordinary dense arrays and plain objects where the behavior is equivalent:
- const nextItems = [...items, ...moreItems];
- const appendedItems = [...items, nextItem];
- const prependedItems = [firstItem, ...items];
- const options = { ...defaults, enabled: true };
+ const nextItems = items.concat(moreItems);
+ const appendedItems = items.concat([nextItem]);
+ const prependedItems = [firstItem].concat(items);
+ const options = Object.assign({}, defaults, { enabled: true });Wrapping nextItem in an array prevents concat from flattening it when the value is itself an array. The rule reports each containing literal once and does not autofix because custom iterators, concat-spreadability, sparse arrays, setters, and proxies can change behavior. Review each diagnostic for equivalent behavior.
legibility/prefer-early-return()
Avoid else branches after an if branch already exits.
do / don't
- if (!user) {
- return null;
- } else {
- return user.name;
- }
+ if (!user) {
+ return null;
+ }
+
+ return user.name;legibility/prefer-flat-map()
Prefer flatMap over map(...).flat().
do / don't
- const permissions = users.map((user) => user.permissions).flat();
+ const permissions = users.flatMap((user) => user.permissions);legibility/prefer-guard-clauses()
Prefer guard clauses over wrapping a whole function body in one branch.
do / don't
- function sendInvite(user) {
- if (user) {
- const email = buildEmail(user);
- deliver(email);
- }
- }
+ function sendInvite(user) {
+ if (!user) return;
+
+ const email = buildEmail(user);
+ deliver(email);
+ }legibility/prefer-object-lookup({options})
Prefer Set, Map, or object lookups over long equality || chains.
options
{min: number}: equality checks required before reporting. Default:3.{operators: string[]}: equality operators that count. Default:["==", "==="].
do / don't
- const isSupported = type === "page" || type === "post" || type === "asset";
+ const supportedTypes = new Set(["page", "post", "asset"]);
+ const isSupported = supportedTypes.has(type);legibility/prefer-positive-condition-names({options})
Prefer positive boolean names over names like isNotReady.
options
{booleanOperators: string[]}: binary operators that mark an initializer as boolean-like.
do / don't
- const isNotReady = status !== "ready";
-
- if (!isNotReady) {
- run();
- }
+ const isReady = status === "ready";
+
+ if (isReady) {
+ run();
+ }legibility/require-executable-shebang({options})
Require configured CLI entry source files to include a Node, Bun, or Deno shebang.
This rule is opt-in because a common source index is not necessarily executable. Enable it only for actual command entry paths.
options
{files: string[]}: source files expected to be executable entries.{runtimes: string[]}: accepted shebang runtimes. Default:["bun", "deno", "node"].
do / don't
- console.log("hello");
+#!/usr/bin/env node
+
+ console.log("hello");legibility/require-filename-matches-dirname({options})
Require filenames to match an explicitly selected schema. The rule is not included in a preset because projects must choose dirname, index, or a custom schema.
options
{schema: "dirname" | "index" | "custom"}: required filename schema.{minDepth: number}: minimum parent depth to check. Default:3.{allowedQualifiers: string[]}:dirnameschema suffixes.{allowedFilenames: string[]}:dirnameschema standalone basenames.{patterns: string[]}: required exact basenames for acustomschema. Use{dirname}as the parent-directory placeholder.
The rule ignores the final JavaScript or TypeScript extension, so the same schema covers .js, .jsx, .ts, and .tsx files.
index schema
The index schema allows only constants, index, index.test, types, utils, and utils.test:
const filenameSchema = { schema: "index", minDepth: 3 };
const filenameRules = {
"legibility/require-filename-matches-dirname": ["error", filenameSchema],
};
export default [
legibility.configs["flat/recommended"],
{ rules: filenameRules },
];- src/components/button/button.ts
- src/components/button/button.test.ts
+ src/components/button/index.ts
+ src/components/button/index.test.tsx
+ src/components/button/utils.tsdirname schema
The dirname schema keeps the existing directory-name convention. It allows button, qualified forms such as button.test, and standalone filenames such as index under src/components/button/:
const filenameSchema = { schema: "dirname", minDepth: 3 };
const filenameRules = {
"legibility/require-filename-matches-dirname": ["error", filenameSchema],
};- src/components/button/useButton.ts
- src/components/button/button.effect.ts
+ src/components/button/button.ts
+ src/components/button/button.test.ts
+ src/components/button/index.tsThe default qualifiers are constants, helpers, spec, styles, test, types, and utils. The default standalone filenames are constants, index, types, and utils. The option arrays replace those defaults.
custom schema
Custom patterns match the basename exactly after replacing {dirname}:
const filenameSchema = {
schema: "custom",
minDepth: 3,
patterns: ["{dirname}", "{dirname}.test", "index", "index.test", "schema"],
};
const filenameRules = {
"legibility/require-filename-matches-dirname": ["error", filenameSchema],
};Operator Options
The operator-counting rules accept the same option shape:
legibility/hoist-if-operatorslegibility/max-expression-operatorslegibility/no-complex-ternarieslegibility/no-computed-values
{
rules: {
"legibility/max-expression-operators": [
"warn",
{
max: 4,
operators: ["&&", "||", "??", "?:", "!", "===", "!=="],
complexity: { "?:": 2 }
}
]
}
}Chain And Count Options
Use max and min to tune rule sensitivity.
{
rules: {
"legibility/max-array-chain-depth": ["warn", { max: 3 }],
"legibility/max-control-flow-depth": ["warn", { max: 2 }],
"legibility/prefer-object-lookup": ["warn", { min: 4 }]
}
}Recipes
The bundled presets check comment quality. They do not ban every comment. Use a session flag or configure no-unmatched-comments when comments need an explicit allow policy.
Block comments during an agent session
Pass --comments=forbid to the changed-file lint command:
npx lint-changed --comments=forbidThe flag enables legibility/no-unmatched-comments as an error for that invocation. It does not change the project config. Every comment in a new file fails. In modified files, only comments that intersect added lines fail, so existing comments outside the session diff remain untouched.
ESLint disable directives cannot suppress the session policy. Parse or configuration failures fail the command, and a pure file rename does not turn existing comments into additions.
Pass the base branch before or after the flag:
npx lint-changed origin/develop --comments=forbidKeep normal comment checks
Run changed-file linting without the flag:
npx lint-changedThis uses the project config. The bundled presets still reject automated attribution, stacked comments, and non-JSDoc multiline blocks.
Allow only marked comments
Configure no-unmatched-comments directly when a repository permits a small set of durable comments:
import legibility from "eslint-plugin-legibility";
+const approvedPrefixes = ["WHY:"];
+const commentOptions = { prefixIdentifiers: approvedPrefixes };
+const approvedCommentRule = ["error", commentOptions];
+const commentRules = {
+ "legibility/no-unmatched-comments": approvedCommentRule,
+};
+const commentConfig = { rules: commentRules };
+
export default [
legibility.configs["flat/recommended"],
+ commentConfig,
];- // Wait before retrying.
+ // WHY: The provider resets its rate-limit window every 30 seconds.
const retryDelayMs = 30_000;Keep CI and pre-commit on repository policy
Omit the session flag from commit and pull-request gates:
npx lint-changed origin/mainRun the same project policy in pre-commit checks:
npx lint-changedThese checks allow comments unless the project config explicitly restricts them. The bundled comment-quality rules still apply. Reserve --comments=forbid for active agent sessions.
Usage
Using With ESLint
Flat config:
import legibility from "eslint-plugin-legibility";
export default [legibility.configs["flat/recommended"]];Configure rules directly:
import legibility from "eslint-plugin-legibility";
export default [
{
plugins: { legibility },
rules: {
"legibility/max-array-chain-depth": ["warn", { max: 2 }],
"legibility/max-expression-operators": ["warn", { max: 4 }],
"legibility/no-quadratic-patterns": "warn",
},
},
];CommonJS compatibility:
const legibility = require("eslint-plugin-legibility");Usage With Oxlint
Oxlint JavaScript plugins use the same ESLint-compatible rule API.
Use a legibility.configs.* preset from eslint-plugin-legibility/oxlint with oxlint.config.ts. For .oxlintrc.json, register the plugin and configure rules explicitly:
{
"jsPlugins": [
{
"name": "legibility",
"specifier": "eslint-plugin-legibility/oxlint"
}
],
"rules": {
"legibility/max-array-chain-depth": ["warn", { "max": 2 }],
"legibility/max-expression-operators": ["warn", { "max": 4 }],
"legibility/no-quadratic-patterns": "warn",
"complexity": ["warn", 20],
"max-lines-per-function": [
"warn",
{
"max": 40,
"skipBlankLines": true,
"skipComments": true,
"IIFEs": true
}
]
}
}Agent Skill
Install the packaged agent skill after installing the npm package:
npx eslint-plugin-legibility-install-skillInstall for a specific agent target:
npx eslint-plugin-legibility-install-skill --target codex
npx eslint-plugin-legibility-install-skill --target claudeAPI
Rules are configured through ESLint or Oxlint rules.
{
"rules": {
"legibility/rule-name": ["warn", { "option": "value" }]
}
}Security Posture
- No runtime dependencies.
- Published package contents are allowlisted with
files. - Releases are tag-triggered and publish GitHub release assets.
- npm publishing uses GitHub Actions trusted publishing with provenance.
- CI runs tests on Node 22, 24, and 26; Docker package-consumer tests cover supported ESLint and Oxlint ranges; compatibility suites run on Bun and Deno.
- Codependence maintains pnpm dependencies, GitHub Actions, and Docker image pins.
- Pastoralist audits CVE overrides in
pnpm-workspace.yamland records their metadata inpackage.json.
Development
The repository uses Mise for Node 26 and Nub for pnpm 11. Nub keeps pnpm-lock.yaml as the package-manager source of truth.
nub install --frozen-lockfile
nub run validateDocker end-to-end tests
Build the package tarball, install it in an isolated consumer, and test ESLint and Oxlint with the default, opt-in, recommended, and strict fixture profiles:
nub run test:e2eBenchmark every engine and fixture setup against one file and a generated 100-file project:
nub run benchmark:e2eThe local image defaults to Node 26, ESLint 9, and Oxlint 1.78. Set E2E_NODE_VERSION, E2E_ESLINT_VERSION, and E2E_OXLINT_VERSION to test another supported combination. CI covers the oldest supported ESLint and Oxlint releases and the current major releases across Node 22, 24, and 26.
The benchmark reports JSON with mean, median, p95, minimum, maximum, and mean-per-file duration. Adjust its sample counts with BENCHMARK_WARMUPS and BENCHMARK_ITERATIONS. Benchmarks report measurements without enforcing timing thresholds. Both commands remove their Compose containers, networks, volumes, and local e2e image after success or failure.
