Skip to content

chore(lint): replace ESLint with type-aware Oxlint and Sketchi guardrails - #359

Merged
graphite-app[bot] merged 1 commit into
mainfrom
chore/oxlint-gate
Oct 10, 2026
Merged

graphite-app[bot] merged 1 commit into
mainfrom
chore/oxlint-gate

Conversation

@anandpant

@anandpant anandpant commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

Replaces ESLint with one type-aware Oxlint gate over the whole repository, and adds a tested Sketchi plugin for the one durable policy the built-in rules don't cover.

  • Tooling:
    • Pinned oxlint 1.87.0, @oxlint/plugins 1.87.0, and oxlint-tsgolint 7.0.2003. 7.0.2004 was published today, inside the repo's 24-hour minimumReleaseAge.
    • Removed eslint, @nx/eslint, @typescript-eslint/parser, eslint-config-prettier, eslint.config.mjs, and the inferred Nx lint target.
    • @nx/eslint-plugin stays only as the Oxlint JS plugin that provides @nx/enforce-module-boundaries. ESLint itself is not installed.
  • .oxlintrc.json: the correctness category as errors, plus a curated set of type-aware TypeScript, import, React-hooks, Unicorn, and Vitest rules. Options: typeAware, denyWarnings, and reportUnusedDisableDirectives: error. Every rule turned off is commented with its reason. .oxlintrc.json replaces eslint.config.mjs in Nx sharedGlobals, so architecture changes still mark every project affected.
  • pnpm run lint: builds the package declarations that type-aware rules resolve (cached Nx build of tag:scope:package, the same prerequisite typecheck already has), then runs oxlint. It takes about 3 seconds over roughly 600 files. CI runs it after the build gate.
  • sketchi/no-react-effects in tools/oxlint/sketchi-plugin.ts:
    • Covers useEffect, useLayoutEffect, and useInsertionEffect.
    • Resolves aliases through scope analysis: renamed imports, namespace and default imports, computed members, destructuring, namespace alias chains, detached references, awaited import("react"), and hook re-exports.
    • Re-exporting React's default or namespace (export { default as R } from "react", export * as R from "react") is reported.
    • So is any other escape of the namespace: exporting it, spreading it, assigning it, passing it to a function, putting it in an array, or laundering it through an assertion. Types, typeof React, void, and JSX member tags such as <React.Fragment> stay allowed.
  • sketchi/no-restricted-import-types: no-restricted-imports does not inspect TypeScript import types, so typeof import("effect") used to slip past the Effect-free policy. This rule mirrors the import policy for import("…") types in every scope; each restricted module covers its subpaths, and @effect covers the whole scope.
  • Both rules are tested with Oxlint's RuleTester (pnpm run test:lint-rules, 48 cases). The cases cover valid code, violations, false positives, and alias and escape forms. False positives include same-named locals and other modules, shadowed parameters, Effect.gen, type-only imports, and prefix look-alikes such as effects-polyfill.
  • Remaining effects as reasoned exceptions:
    • Icon detail: a modal focus trap and focus restore on a window keydown listener.
    • The Code Mode export harness: the window bridge that Browser Rendering polls.
    • An Excalidraw test double.
    • Vendored AI Elements, through a directory override that also relaxes upstream's React idioms.
  • Suppressions: every suppression is a one-line oxlint-disable-next-line <rule> -- <reason>. A graph test fails on any directive without a reason.
  • tools/project-graph.test.ts:
    • The ESLint lintText probes become probes through the real Oxlint CLI and config. They cover boundaries in source, Vitest config, and Storybook files; the workspace-input allowances; Effect-free projects, including deep subpaths and import types; stable versus unstable Effect; the reviewed-adapter path; Zod, including deep subpaths; and React effects. A mutation check confirmed these probes fail when a guardrail is removed.
    • The graph test also checks that every scope restricting imports restricts exactly the same modules for import types.
    • The regex scans for Effect, Zod, and unstable imports are replaced by lint rules plus assertions that the lint overrides match the project classification and the schema-boundary file list exactly.
    • Zod is also barred from every workspace manifest, and unstable adapters are an explicit file inventory.
  • Docs: README, the architecture guide, and AGENTS.md describe the gate. AGENTS.md Proof gains pnpm run lint, and its delegated-agent model line now says gpt-6.1-sol (gpt-6-astra for harder slices).

Decisions

  • Nx boundaries:
    • The scope:*/type:* constraints and enforceBuildableLibDependency are unchanged.
    • Both workspace-input allow entries are still required; without them, all five Vite configs fail.
    • The Studio checkDynamicDependenciesExceptions entry is removed. refactor(playground): dedupe server handlers and build layers once #341 deleted the import("@sketchi/studio-projects/server") type queries that made Nx record a dynamic Playground→Studio edge. That edge is now static, and lint is clean without the exception.
  • Effect guardrails:
    • Import policies use Oxlint's built-in no-restricted-imports: no Zod; unstable Effect only in src/internal/effect-unstable-*.ts; no Effect in pure or framework-native projects (now including packages/icons/catalog from refactor(icons): extract the icon catalog into @sketchi/icon-catalog #354) except diagram-core's schema-boundary files. The rule also catches re-exports and dynamic import().
    • Patterns are effect/**, @effect/**, zod/**, and effect/unstable/**. Oxlint's group globs match one segment per *, unlike ESLint's gitignore matching, so deep paths such as @effect/platform-node/NodeRuntime need **. A graph test rejects single-segment patterns, and CLI probes cover deep paths and import types.
    • The runtime-boundary inventory (run*, ManagedRuntime, NodeRuntime.runMain) and the Promise-site inventory stay as graph tests. They resolve signatures through the TypeScript checker, which catches cross-module aliases and parameters typed as the Effect namespace. Oxlint JS plugins have no type information and tsgolint has no custom-rule API, so moving them would weaken them.
  • Prismantix rules evaluated and not adopted:
    • no-legacy-schema-classes: Schema.Class is Sketchi's record convention (115 uses) and a supported Effect v4 API.
    • no-effect-sleep-in-tests: Sketchi's test sleeps run under TestClock, so they are deterministic, or poll real child processes in it.live.

Open questions from #281

  • Strict rules now versus later. Enabled now: correctness plus no-explicit-any, no-floating-promises, no-misused-promises, switch-exhaustiveness-check, return-await, only-throw-error, prefer-promise-reject-errors, no-unnecessary-type-assertion, import/no-cycle, import/no-duplicates, rules-of-hooks, eqeqeq, and prefer-node-protocol. These were clean or remediated in the previous PR. Deferred until measured remediation (counts on this tree): no-unsafe-* about 1,800, strict-boolean-expressions 633, require-await 129, no-confusing-void-expression 113, unicorn/no-array-sort 92, node/no-process-env 90, no-await-in-loop 76, no-unnecessary-condition 72, no-non-null-assertion 25, no-deprecated 21.
  • Nx target or repo-wide gate. Repo-wide gate only. Oxlint lints the whole tree in one process faster than Nx can schedule per-project targets, so a second per-project target would only add duplicate work. It also covers scripts/ and the root tools/ files, which the per-project ESLint target never linted.
  • Oxfmt exclusions. Handled in the Oxfmt PR on top of this one. Lint ignores only generated output: routeTree.gen.ts, Wrangler cloudflare-workers.d.ts, dist, .output, .wrangler, coverage, Storybook builds, and .memory.
  • React effects. The adapters and violations are listed above. The product violations were refactored in the first PR of this stack.

How it was verified

  • pnpm run lint: zero diagnostics.
  • pnpm run test:lint-rules: 48 tests. tools/tsconfig.json (from chore(tools): typecheck repository tools in CI #352) now includes tools/oxlint, so pnpm run test:tools typechecks the plugin and its tests.
  • pnpm run test:tools: 183 tests, including the Oxlint probes.
  • nx affected -t typecheck,test: all projects.
  • pnpm run toolchain:verify.

Refs #281

anandpant commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • merge - adds this PR to the back of the merge queue
  • fast - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@socket-security

socket-security Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedoxlint@​1.87.0991009195100
Addedoxlint-tsgolint@​7.0.20031001009793100
Added@​oxlint/​plugins@​1.87.01001009996100

View full report

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@anandpant
anandpant force-pushed the fix/type-aware-lint-findings branch 2 times, most recently from 9847b20 to 9d27d4e Compare October 10, 2026 19:23
@graphite-app

graphite-app Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Merge activity

…ails (#359)

## What changed

Replaces ESLint with one type-aware Oxlint gate over the whole repository, and adds a tested Sketchi plugin for the one durable policy the built-in rules don't cover.

- **Tooling:**
  - Pinned `oxlint` 1.87.0, `@oxlint/plugins` 1.87.0, and `oxlint-tsgolint` 7.0.2003. 7.0.2004 was published today, inside the repo's 24-hour `minimumReleaseAge`.
  - Removed `eslint`, `@nx/eslint`, `@typescript-eslint/parser`, `eslint-config-prettier`, `eslint.config.mjs`, and the inferred Nx `lint` target.
  - `@nx/eslint-plugin` stays only as the Oxlint JS plugin that provides `@nx/enforce-module-boundaries`. ESLint itself is not installed.
- **`.oxlintrc.json`:** the correctness category as errors, plus a curated set of type-aware TypeScript, import, React-hooks, Unicorn, and Vitest rules. Options: `typeAware`, `denyWarnings`, and `reportUnusedDisableDirectives: error`. Every rule turned off is commented with its reason. `.oxlintrc.json` replaces `eslint.config.mjs` in Nx `sharedGlobals`, so architecture changes still mark every project affected.
- **`pnpm run lint`:** builds the package declarations that type-aware rules resolve (cached Nx `build` of `tag:scope:package`, the same prerequisite `typecheck` already has), then runs `oxlint`. It takes about 3 seconds over roughly 600 files. CI runs it after the build gate.
- **`sketchi/no-react-effects`** in `tools/oxlint/sketchi-plugin.ts`:
  - Covers `useEffect`, `useLayoutEffect`, and `useInsertionEffect`.
  - Resolves aliases through scope analysis: renamed imports, namespace and default imports, computed members, destructuring, namespace alias chains, detached references, awaited `import("react")`, and hook re-exports.
  - Re-exporting React's default or namespace (`export { default as R } from "react"`, `export * as R from "react"`) is reported.
  - So is any other escape of the namespace: exporting it, spreading it, assigning it, passing it to a function, putting it in an array, or laundering it through an assertion. Types, `typeof React`, `void`, and JSX member tags such as `<React.Fragment>` stay allowed.
- **`sketchi/no-restricted-import-types`:** `no-restricted-imports` does not inspect TypeScript import types, so `typeof import("effect")` used to slip past the Effect-free policy. This rule mirrors the import policy for `import("…")` types in every scope; each restricted module covers its subpaths, and `@effect` covers the whole scope.
- Both rules are tested with Oxlint's `RuleTester` (`pnpm run test:lint-rules`, 48 cases). The cases cover valid code, violations, false positives, and alias and escape forms. False positives include same-named locals and other modules, shadowed parameters, `Effect.gen`, type-only imports, and prefix look-alikes such as `effects-polyfill`.
- **Remaining effects as reasoned exceptions:**
  - Icon detail: a modal focus trap and focus restore on a window `keydown` listener.
  - The Code Mode export harness: the window bridge that Browser Rendering polls.
  - An Excalidraw test double.
  - Vendored AI Elements, through a directory override that also relaxes upstream's React idioms.
- **Suppressions:** every suppression is a one-line `oxlint-disable-next-line <rule> -- <reason>`. A graph test fails on any directive without a reason.
- **`tools/project-graph.test.ts`:**
  - The ESLint `lintText` probes become probes through the real Oxlint CLI and config. They cover boundaries in source, Vitest config, and Storybook files; the workspace-input allowances; Effect-free projects, including deep subpaths and import types; stable versus unstable Effect; the reviewed-adapter path; Zod, including deep subpaths; and React effects. A mutation check confirmed these probes fail when a guardrail is removed.
  - The graph test also checks that every scope restricting imports restricts exactly the same modules for import types.
  - The regex scans for Effect, Zod, and unstable imports are replaced by lint rules plus assertions that the lint overrides match the project classification and the schema-boundary file list exactly.
  - Zod is also barred from every workspace manifest, and unstable adapters are an explicit file inventory.
- **Docs:** README, the architecture guide, and AGENTS.md describe the gate. AGENTS.md Proof gains `pnpm run lint`, and its delegated-agent model line now says `gpt-6.1-sol` (`gpt-6-astra` for harder slices).

## Decisions

- **Nx boundaries:**
  - The `scope:*`/`type:*` constraints and `enforceBuildableLibDependency` are unchanged.
  - Both workspace-input `allow` entries are still required; without them, all five Vite configs fail.
  - The Studio `checkDynamicDependenciesExceptions` entry is removed. #341 deleted the `import("@sketchi/studio-projects/server")` type queries that made Nx record a dynamic Playground→Studio edge. That edge is now static, and lint is clean without the exception.
- **Effect guardrails:**
  - Import policies use Oxlint's built-in `no-restricted-imports`: no Zod; unstable Effect only in `src/internal/effect-unstable-*.ts`; no Effect in pure or framework-native projects (now including `packages/icons/catalog` from #354) except diagram-core's schema-boundary files. The rule also catches re-exports and dynamic `import()`.
  - Patterns are `effect/**`, `@effect/**`, `zod/**`, and `effect/unstable/**`. Oxlint's `group` globs match one segment per `*`, unlike ESLint's gitignore matching, so deep paths such as `@effect/platform-node/NodeRuntime` need `**`. A graph test rejects single-segment patterns, and CLI probes cover deep paths and import types.
  - The runtime-boundary inventory (`run*`, `ManagedRuntime`, `NodeRuntime.runMain`) and the Promise-site inventory stay as graph tests. They resolve signatures through the TypeScript checker, which catches cross-module aliases and parameters typed as the Effect namespace. Oxlint JS plugins have no type information and tsgolint has no custom-rule API, so moving them would weaken them.
- **Prismantix rules evaluated and not adopted:**
  - `no-legacy-schema-classes`: `Schema.Class` is Sketchi's record convention (115 uses) and a supported Effect v4 API.
  - `no-effect-sleep-in-tests`: Sketchi's test sleeps run under `TestClock`, so they are deterministic, or poll real child processes in `it.live`.

## Open questions from #281

- **Strict rules now versus later.** Enabled now: correctness plus `no-explicit-any`, `no-floating-promises`, `no-misused-promises`, `switch-exhaustiveness-check`, `return-await`, `only-throw-error`, `prefer-promise-reject-errors`, `no-unnecessary-type-assertion`, `import/no-cycle`, `import/no-duplicates`, `rules-of-hooks`, `eqeqeq`, and `prefer-node-protocol`. These were clean or remediated in the previous PR. Deferred until measured remediation (counts on this tree): `no-unsafe-*` about 1,800, `strict-boolean-expressions` 633, `require-await` 129, `no-confusing-void-expression` 113, `unicorn/no-array-sort` 92, `node/no-process-env` 90, `no-await-in-loop` 76, `no-unnecessary-condition` 72, `no-non-null-assertion` 25, `no-deprecated` 21.
- **Nx target or repo-wide gate.** Repo-wide gate only. Oxlint lints the whole tree in one process faster than Nx can schedule per-project targets, so a second per-project target would only add duplicate work. It also covers `scripts/` and the root `tools/` files, which the per-project ESLint target never linted.
- **Oxfmt exclusions.** Handled in the Oxfmt PR on top of this one. Lint ignores only generated output: `routeTree.gen.ts`, Wrangler `cloudflare-workers.d.ts`, `dist`, `.output`, `.wrangler`, coverage, Storybook builds, and `.memory`.
- **React effects.** The adapters and violations are listed above. The product violations were refactored in the first PR of this stack.

## How it was verified

- `pnpm run lint`: zero diagnostics.
- `pnpm run test:lint-rules`: 48 tests. `tools/tsconfig.json` (from #352) now includes `tools/oxlint`, so `pnpm run test:tools` typechecks the plugin and its tests.
- `pnpm run test:tools`: 183 tests, including the Oxlint probes.
- `nx affected -t typecheck,test`: all projects.
- `pnpm run toolchain:verify`.

Refs #281
@graphite-app
graphite-app Bot force-pushed the fix/type-aware-lint-findings branch from 9d27d4e to af2123b Compare October 10, 2026 19:37
@graphite-app
graphite-app Bot force-pushed the chore/oxlint-gate branch from 5087b20 to 8dd19fb Compare October 10, 2026 19:38
Base automatically changed from fix/type-aware-lint-findings to main October 10, 2026 19:39
@graphite-app
graphite-app Bot merged commit 8dd19fb into main Oct 10, 2026
4 checks passed
@graphite-app
graphite-app Bot deployed to production October 10, 2026 19:40 Active
@graphite-app
graphite-app Bot deployed to production October 10, 2026 19:40 Active
@graphite-app
graphite-app Bot deployed to production October 10, 2026 19:40 Active
@graphite-app
graphite-app Bot deployed to production October 10, 2026 19:40 Active
@graphite-app
graphite-app Bot deployed to production October 10, 2026 19:40 Active
@graphite-app
graphite-app Bot deleted the chore/oxlint-gate branch October 10, 2026 19:40
@github-actions

Copy link
Copy Markdown
Contributor

Sketchi Eval Harness Preview

Status: deleted

@github-actions

Copy link
Copy Markdown
Contributor

Sketchi Excalidraw Workspace Preview

Status: deleted

@github-actions

Copy link
Copy Markdown
Contributor

Sketchi Icons Preview

Status: deleted

@github-actions

Copy link
Copy Markdown
Contributor

Sketchi Web Preview

Status: deleted

@github-actions

Copy link
Copy Markdown
Contributor

Sketchi Playground / Studio Preview

Status: deleted

This branch was successfully deployed

2 active deployments
staging — 8dd19fb5 Deployed Oct 10, 2026 by graphite-app[bot] via cleanup-preview (eval-harness) #412
production — 8dd19fb5 Deployed Oct 10, 2026 by graphite-app[bot] via deploy (eval-harness) #109
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant