refactor: make lint workflows Oxlint-only

This commit is contained in:
Turtle
2026-08-09 14:55:15 +08:00
parent 8c124f84b6
commit 7016ad93de
24 changed files with 323 additions and 253 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-06-11-quality-gates.md
2026-06-11-quality-gates.md: 60db7ba5cfa8184c0fcce764aa027f32a9b721ab
2026-06-11-quality-gates.zh.md: 10585977820ba8e4f4bc6e251d484dc657d0558c
2026-06-11-quality-gates.md: 83cba5c867f71471d421ab935e1f53b430b01c43
2026-06-11-quality-gates.zh.md: 1162b3836424bd47a8d35bd754a7983ac2e9bebe
@@ -19,7 +19,7 @@ Every mechanically checkable AGENTS.md promise gets a command that exits non-zer
- jscpd detects cross-file clones in package production TypeScript and repository scripts; narrow source-range exceptions document deliberately parallel implementations.
- Per-file 100% coverage on `packages/*/*/src` (v8); unreachable defensive guards carry `/* v8 ignore */ ` with stated reasons instead of deletion.
- knip (dead code/deps), publint (package correctness), workspace constraints (workspace rules: private, cordis peer+dev, uniform version, ESM), and a NodeNext consumer typecheck for built package declarations.
- lefthook pre-commit applies formatting-only ESLint fixes before Oxlint validation and native fixes, rejects staged whitespace, and checks the vendor manifest; pre-push runs incremental typecheck. CI runs the full matrix on node 22.19/24/26 plus built application smokes for the Headless, TUI, ACP, JSON-RPC, workflow, and code-runtime entry paths.
- lefthook pre-commit applies project-free Oxlint validation and [safe fixes with a bounded retry](2026-08-09-oxlint-only-fix-workflow.md), rejects staged whitespace, and checks the vendor manifest; pre-push runs incremental typecheck. CI runs the full matrix on node 22.19/24/26 plus built application smokes for the Headless, TUI, ACP, JSON-RPC, workflow, and code-runtime entry paths.
## Consequences
@@ -19,7 +19,7 @@ Status: implemented
- jscpd 检测包的生产 TypeScript 代码与仓库脚本中的跨文件克隆;窄范围的源码区间例外用于记录有意为之的并行实现。
- `packages/*/*/src` 下按文件 100% 覆盖率(v8);不可达的防御性守卫使用 `/* v8 ignore */ ` 并注明理由,而非删除。
- knip(死代码/依赖)、publint(包的正确性)、workspace 约束(workspace 规则:private、cordis peer+dev、统一版本、ESM),以及对构建出的包声明文件进行 NodeNext 消费方类型检查。
- lefthook pre-commit 先应用仅用于格式化的 ESLint 修复,再执行 Oxlint 验证和原生修复,拒绝已暂存的空白问题并检查 vendor manifest(元数据清单);pre-push 运行增量类型检查。CI 在 Node 22.19/24/26 上运行完整矩阵,并对 Headless、TUI、ACPAgent Client Protocol)、JSON-RPC、工作流和代码运行时入口路径执行已构建应用的冒烟测试。
- lefthook pre-commit 执行不加载项目的 Oxlint 验证,并应用带[一次有界重试](2026-08-09-oxlint-only-fix-workflow.md)的安全修复,拒绝已暂存的空白问题并检查 vendor manifest(元数据清单);pre-push 运行增量类型检查。CI 在 Node 22.19/24/26 上运行完整矩阵,并对 Headless、TUI、ACPAgent Client Protocol)、JSON-RPC、工作流和代码运行时入口路径执行已构建应用的冒烟测试。
## 后果
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-fast-local-git-hooks.md
2026-07-22-fast-local-git-hooks.md: af8a9380fc3cbf4f672ca0b32faa7b187522cf11
2026-07-22-fast-local-git-hooks.zh.md: 1b2c3bf42a65f35d96c266f5f3afb63f26039388
2026-07-22-fast-local-git-hooks.md: c3d3579b2e91419fad64f0e64a10926d2362881e
2026-07-22-fast-local-git-hooks.zh.md: f85daa9c74a23987cc560f14c5e087fdfb3d7f5e
@@ -12,7 +12,7 @@ Fast hooks still need to reject cheap, high-confidence defects before work leave
## Decision
[lefthook.yml](../../../../lefthook.yml) keeps both hooks as bounded local checkpoints. Pre-commit runs sequentially: a formatting-only ESLint config fixes and re-stages changed JavaScript and TypeScript, a project-free [Oxlint](2026-07-29-oxlint-linter.md) profile validates those files and applies native safe fixes, `git diff --cached --check` rejects staged whitespace errors, and the vendor manifest guard checks vendored-source metadata. Pre-push runs `pnpm run typecheck`, which prepares the generated Host TypeRT contracts before the Client incremental typecheck.
[lefthook.yml](../../../../lefthook.yml) keeps both hooks as bounded local checkpoints. Pre-commit runs sequentially: a project-free [Oxlint](2026-07-29-oxlint-linter.md) profile validates changed JavaScript and TypeScript, applies safe fixes with a [bounded retry](2026-08-09-oxlint-only-fix-workflow.md), and re-stages them; `git diff --cached --check` rejects staged whitespace errors, and the vendor manifest guard checks vendored-source metadata. Pre-push runs `pnpm run typecheck`, which prepares the generated Host TypeRT contracts before the Client incremental typecheck.
Pre-commit does not run type analysis, tests, snapshots, documentation checks, builds, hygiene, or the gate scheduler. Pre-push adds only the Host contract build required by repository typecheck. The opt-in `check:all` package script selects the `check-all` scheduler inventory in [scripts/run-gates.ts](../../../../scripts/run-gates.ts) independently of the hooks; it is a contributor command, not an agent instruction.
@@ -27,10 +27,10 @@ This decision supersedes the local-hook portion of [Parallel pre-push gates](202
- **Keep the full pre-push suite and optimize its scheduler** — preserves the earliest exhaustive signal but still repeats agent-selected evidence and CI, while unrelated failures continue blocking publication.
- **Remove pre-push entirely** — makes pushes cheapest but loses the fast cross-file guarantee that TypeScript provides after several commits.
- **Keep typecheck in pre-commit** — catches type errors earlier but charges every intermediate commit instead of one push; staged lint already covers the commit-local syntax and style boundary.
- **Make staged lint check-only** — avoids hook-side mutation, but contributors intentionally retain the auto-fix workflow; the formatting-only pass and Lefthook's `stage_fixed` preserve it without making ESLint a repository correctness runner or duplicating `git add`.
- **Make staged lint check-only** — avoids hook-side mutation, but contributors intentionally retain the auto-fix workflow; Oxlint's bounded retry and Lefthook's `stage_fixed` preserve it without a separate formatter or duplicate `git add`.
## Consequences
Normal commits take the project-free staged formatter-and-lint critical path, and warm pushes take the prepared incremental typecheck critical path. Contributors retain a one-command opt-in rehearsal without widening the hook critical paths or the agent-required validation set. Hook latency is observed in development and PR evidence rather than enforced by a timing test whose result would depend on host load and cache state.
Normal commits take the project-free staged Oxlint fix-and-validate critical path, and warm pushes take the prepared incremental typecheck critical path. Contributors retain a one-command opt-in rehearsal without widening the hook critical paths or the agent-required validation set. Hook latency is observed in development and PR evidence rather than enforced by a timing test whose result would depend on host load and cache state.
Local publication no longer proves the exhaustive repository matrix. Agents must select relevant behavioral evidence, reviewers must evaluate whether that selection matches the diff, and CI supplies the comprehensive signal once per pushed revision.
@@ -12,7 +12,7 @@ agent(智能体)已经会运行能够覆盖自身改动的测试和检查,
## 决策
[lefthook.yml](../../../../lefthook.yml) 将两个钩子都保留为有界的本地检查点。Pre-commit 按顺序运行:仅用于格式化的 ESLint 配置修复改动过的 JavaScript 和 TypeScript 文件并重新暂存,不加载项目的 [Oxlint](2026-07-29-oxlint-linter.md) 配置验证这些文件并应用原生安全修复,`git diff --cached --check` 拒绝暂存 diff 中的空白错误,vendor manifest(元数据清单)守卫检查 vendor 源码元数据。Pre-push 运行 `pnpm run typecheck`;该命令会先准备好生成的 Host TypeRT 约定,再运行 Client 增量类型检查。
[lefthook.yml](../../../../lefthook.yml) 将两个钩子都保留为有界的本地检查点。Pre-commit 按顺序运行:不加载项目的 [Oxlint](2026-07-29-oxlint-linter.md) 配置验证改动过的 JavaScript 和 TypeScript 文件,应用带[一次有界重试](2026-08-09-oxlint-only-fix-workflow.md)的安全修复,并重新暂存这些文件;`git diff --cached --check` 拒绝暂存 diff 中的空白错误,vendor manifest(元数据清单)守卫检查 vendor 源码元数据。Pre-push 运行 `pnpm run typecheck`;该命令会先准备好生成的 Host TypeRT 约定,再运行 Client 增量类型检查。
Pre-commit 不运行类型分析、测试、快照、文档检查、构建、`hygiene` 或门禁调度器。Pre-push 只增加仓库类型检查所需的 Host 约定构建。可选运行的 `check:all` 包脚本独立于这些钩子,从 [scripts/run-gates.ts](../../../../scripts/run-gates.ts) 中选择 `check-all` 调度器清单;它是贡献者命令,而非对 agent 的指令。
@@ -27,10 +27,10 @@ agent 检查待推送的 diff,并仅运行一次能够覆盖其行为的最小
- **保留全量 pre-push 套件并优化其调度器**——能够最早提供全面信号,但仍会重复 agent 已选取的证据和 CI,且无关失败仍会阻塞推送。
- **完全移除 pre-push**——推送成本最低,但会失去 TypeScript 在多个提交之后提供的快速跨文件保证。
- **在 pre-commit 中保留类型检查**——更早捕获类型错误,但每次中间提交都要承担开销,而不是只在推送时运行一次;暂存文件 lint 已经覆盖提交本身的语法与风格边界。
- **将暂存文件 lint 设为仅检查模式**——避免钩子修改文件,但贡献者有意保留自动修复工作流;仅用于格式化的流程和 Lefthook 的 `stage_fixed` 会保留该工作流,而不会让 ESLint 成为仓库正确性检查运行器,也无需重复执行 `git add`
- **将暂存文件 lint 设为仅检查模式**——避免钩子修改文件,但贡献者有意保留自动修复工作流;Oxlint 的一次有界重试和 Lefthook 的 `stage_fixed` 会保留该工作流,无需单独的格式化器,也无需重复执行 `git add`
## 结果
普通提交的关键路径是不加载项目的暂存文件格式化与 lint,缓存已预热时推送的关键路径是经过准备的增量类型检查。贡献者仍可选择用一条命令完整演练,且不会扩展钩子关键路径或 agent 必须运行的验证集合。钩子耗时只作为开发观察数据和 PR(Pull Request)证据记录,不设置会受主机负载与缓存状态影响的计时测试。
普通提交的关键路径是不加载项目的暂存文件 Oxlint 修复与验证,缓存已预热时推送的关键路径是经过准备的增量类型检查。贡献者仍可选择用一条命令完整演练,且不会扩展钩子关键路径或 agent 必须运行的验证集合。钩子耗时只作为开发观察数据和 PR(Pull Request)证据记录,不设置会受主机负载与缓存状态影响的计时测试。
从本地推送成功不再能证明仓库完整矩阵已通过。agent 必须选择相关的行为证据,评审人必须判断该选择是否与 diff 相符,CI 则对每个推送版本提供一次全面信号。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-29-oxlint-linter.md
2026-07-29-oxlint-linter.md: 3e3de24be33b8f9461cea2967b0fa18f59ddb52a
2026-07-29-oxlint-linter.zh.md: dae8c7451bd3fbe9296508491aaa51f1118abe92
2026-07-29-oxlint-linter.md: 15bdd4302745fdf3d1a2bd0d28fbc3d4b9fc50bb
2026-07-29-oxlint-linter.zh.md: 8e3d69cdce2cc05346149acbd49f9b0b75ac6098
@@ -12,25 +12,25 @@ A faster runner cannot justify losing rules. The migration must preserve the str
## Decision
The root [`.oxlintrc.json`](../../../../.oxlintrc.json) is the authoritative type-aware repository lint configuration. The project-free [`.oxlintrc.staged.json`](../../../../.oxlintrc.staged.json) profile inherits its source rules but disables type analysis for the bounded pre-commit path. The `lint` package script, gate scheduler, CI, and lefthook invoke Oxlint through [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts); `lint:fix` and lefthook first invoke the formatting-only [`eslint.format.config.mjs`](../../../../eslint.format.config.mjs). The direct `eslint` and `@typescript-eslint/parser` development dependencies exist only for this parser-without-project formatting pass; their exact versions pin the tested parser/fixer pairing, and that config contains no correctness or type-aware rules.
The root [`.oxlintrc.json`](../../../../.oxlintrc.json) is the authoritative type-aware repository lint configuration. The project-free [`.oxlintrc.staged.json`](../../../../.oxlintrc.staged.json) profile inherits its source rules, disables type analysis for the bounded pre-commit path, and re-includes preserved TypeGraph fixtures that the type-aware backend cannot analyze. The `lint` and `lint:fix` package scripts, gate scheduler, CI, and lefthook invoke Oxlint through [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts); the [Oxlint-only fix workflow](2026-08-09-oxlint-only-fix-workflow.md) owns multipass plugin fixes and supersedes the separate formatting fallback.
`options.typeAware` enables `oxlint-tsgolint`. Its backend performs per-file TypeScript-project discovery: package sources use their package projects, host tests/examples/website use `tsconfig.host.json`, and client tests plus `scripts/client-bundle-purity.spec.ts` use `tsconfig.client.json`. The program-less root solution is never flattened. Oxlint's `--tsconfig` override affects import resolution but is ignored by type-aware linting, so this repository does not set it. The configuration explicitly carries the migrated strict-type-checked rules and repository overrides instead of enabling broad Oxlint categories whose contents may change. `typescript/no-unnecessary-condition` remains enabled from Oxlint's nursery set because it was an enforced repository rule before migration.
Oxlint's JavaScript-plugin compatibility layer runs `@stylistic/eslint-plugin` and `eslint-plugin-sonarjs` so the existing formatting and file-local duplicate-logic rules remain enforced. The compatibility layer reports `@stylistic` violations but does not execute their fixers, so the formatting-only ESLint pass owns only the corresponding auto-fixes; an executable parity check keeps those fixable rule definitions aligned while `max-len` remains validation-only. Owned-source suppressions use `oxlint-*` directives and the `typescript/*` namespace, and unused directives remain warnings; vendored sources keep their upstream directives because Oxlint excludes `vendor/**`.
Oxlint's JavaScript-plugin compatibility layer runs `@stylistic/eslint-plugin` and `eslint-plugin-sonarjs` so the existing formatting and file-local duplicate-logic rules remain enforced. The compatibility layer reports `@stylistic` violations and executes their safe fixes; `max-len` remains validation-only. Owned-source suppressions use `oxlint-*` directives and the `typescript/*` namespace, and unused directives remain warnings; vendored sources keep their upstream directives because Oxlint excludes `vendor/**`.
CI does not restore or save a lint-result cache. `DSH_OXLINT_THREADS` makes the shared runner pass the same bound to Oxlint's `--threads` option and the type-aware backend's `GOMAXPROCS` environment variable; ordinary local runs use both defaults. Pre-commit applies the formatting-only ESLint fixes, runs project-free Oxlint validation and native safe fixes, accepts selections containing only ignored files, and re-stages the result through lefthook. Public `lint` and CI retain the complete type-aware rules after preparing generated declarations.
CI does not restore or save a lint-result cache. `DSH_OXLINT_THREADS` makes the shared runner pass the same bound to Oxlint's `--threads` option and the type-aware backend's `GOMAXPROCS` environment variable; ordinary local runs use both defaults. Pre-commit runs project-free Oxlint validation and safe fixes with one bounded retry, accepts selections containing only ignored files, and re-stages the result through lefthook. Public `lint` and CI retain the complete type-aware rules after preparing generated declarations.
## Verification
The migrated configuration reports the same clean owned-source baseline after resolving two analyzer differences: one redundant test assertion was removed, while one structural cast required by `tsc` carries a narrow Oxlint suppression. A one-time audit against the exact deleted ESLint configuration blob established source 88-to-88, examples 87-to-87, and tests 83-to-83 after the rule-name translations. The committed fingerprint pins those audited Oxlint profiles and the complete override shape; it neither executes the deleted configuration nor propagates later upstream preset changes. Evaluating `typescript-eslint@8.61.0` also confirms that `strictTypeChecked` did not enable `@typescript-eslint/no-empty-function`; the deleted tests-only `off` entry was inert.
Executable contract tests require type-aware diagnostics from the package, host, and client projects; assert the client-only script's project; reject unmatched fallback analysis; and exercise the Stylistic, SonarJS, and nursery compatibility paths. They also pin the staged profile's project-free inheritance, unused-suppression reporting, ignored-only staged selections, formatter/validator rule parity, and final formatted bytes. Runner tests pin both worker controls, and typecheck confirms that migration-driven source edits preserve the TypeScript programs.
Executable contract tests require type-aware diagnostics from the package, host, and client projects; assert the client-only script's project; reject unmatched fallback analysis; and exercise the Stylistic, SonarJS, and nursery compatibility paths. They also pin the staged profile's project-free inheritance and TypeGraph coverage, unused-suppression reporting, ignored-only staged selections, the complete Stylistic rule set, and convergent final formatted bytes. Runner tests pin both worker controls, and typecheck confirms that migration-driven source edits preserve the TypeScript programs.
## Alternatives considered
**Run both linters repository-wide.** Every correctness rule is available through Oxlint's native rules, nursery rule, or JavaScript-plugin compatibility layer. A repository-wide ESLint fallback would preserve the slower project-service setup and two correctness configurations without adding a check; the retained ESLint pass is deliberately limited to project-free staged formatting.
**Run both linters repository-wide.** Every correctness rule is available through Oxlint's native rules, nursery rule, or JavaScript-plugin compatibility layer. A repository-wide ESLint fallback would preserve the slower project-service setup and two correctness configurations without adding a check.
**Rely on compatibility-layer fixes.** The layer reports the established `@stylistic` rules but does not apply their fixes under either Oxlint fix mode. Keeping the narrow staged formatter preserves the contributor contract without broadening ESLint back into a repository linter.
**Use a separate formatter.** The migration retained a narrow ESLint pass because the compatibility-layer fixes were treated as unavailable. The [Oxlint-only fix workflow](2026-08-09-oxlint-only-fix-workflow.md) supersedes that part of the decision with one bounded retry after the pinned toolchain proved it could execute the same fixes.
**Drop @stylistic or SonarJS rules that are not native.** This would remove dependencies but weaken the mechanical quality contract. The compatibility layer preserves those rules until native replacements can be evaluated as a separate decision.
@@ -42,4 +42,4 @@ Local migration measurements reduced a clean type-aware lint run from about 61 s
Type-aware diagnostics now come from the TypeScript Go analyzer bundled through `oxlint-tsgolint`, so edge-case inference can differ from typescript-eslint even when `tsc` accepts the same program. Lint and typecheck remain separate required evidence.
The JavaScript-plugin compatibility API, staged profile, and staged formatter are additional boundaries to maintain. Commits defer type-aware diagnostics to public lint and CI, pay one project-free ESLint startup before Oxlint, and avoid depending on generated declarations. The root development graph retains ESLint plus the TypeScript parser. Repository-wide validation, type-aware analysis, cache policy, worker control, and inline directives remain Oxlint-owned.
The JavaScript-plugin compatibility API and staged profile are additional boundaries to maintain. Commits defer type-aware diagnostics to public lint and CI and avoid depending on generated declarations. Repository-wide validation, fixes, type-aware analysis, cache policy, worker control, and inline directives remain Oxlint-owned.
@@ -12,25 +12,25 @@ Status: implemented
## 决策
根目录的 [`.oxlintrc.json`](../../../../.oxlintrc.json) 是仓库类型感知 lint 配置的权威来源。不加载项目的 [`.oxlintrc.staged.json`](../../../../.oxlintrc.staged.json) 配置继承其源码规则,但会为有界的 pre-commit 路径禁用类型分析`lint` 包脚本、门禁调度器、CI 和 lefthook 通过 [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts) 调用 Oxlint`lint:fix` 和 lefthook 会先调用仅用于格式化的 [`eslint.format.config.mjs`](../../../../eslint.format.config.mjs)。直接的 `eslint``@typescript-eslint/parser` 开发依赖仅用于这次不加载项目的格式化流程;其精确版本锁定经过测试的解析器与修复器配对,该配置不包含正确性规则或类型感知规则
根目录的 [`.oxlintrc.json`](../../../../.oxlintrc.json) 是仓库类型感知 lint 配置的权威来源。不加载项目的 [`.oxlintrc.staged.json`](../../../../.oxlintrc.staged.json) 配置继承其源码规则,为有界的 pre-commit 路径禁用类型分析,并重新纳入类型感知后端无法分析但需保留的 TypeGraph fixture(测试前置数据)。`lint``lint:fix` 包脚本、门禁调度器、CI 和 lefthook 通过 [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts) 调用 Oxlint[仅使用 Oxlint 的修复工作流](2026-08-09-oxlint-only-fix-workflow.md)负责多轮插件修复,并取代单独的格式化回退路径
`options.typeAware` 启用 `oxlint-tsgolint`。其后端按文件发现 TypeScript 项目:包源码使用各自的包项目,host 测试、示例和网站使用 `tsconfig.host.json`client 测试及 `scripts/client-bundle-purity.spec.ts` 使用 `tsconfig.client.json`。不含程序的根解决方案绝不会被扁平化。Oxlint 的 `--tsconfig` 覆盖项会影响导入解析,但类型感知 lint 会忽略它,因此本仓库不设置该选项。该配置显式载入迁移后的严格类型检查规则和仓库覆盖配置,而不启用内容可能发生变化的 Oxlint 宽泛类别。`typescript/no-unnecessary-condition` 仍从 Oxlint 的 nursery 规则集中启用,因为它在迁移前就是仓库强制执行的规则。
Oxlint 的 JavaScript 插件兼容层运行 `@stylistic/eslint-plugin``eslint-plugin-sonarjs`,从而继续强制执行现有的格式和文件内重复逻辑规则。兼容层会报告 `@stylistic` 违规,但不会执行其修复器,因此仅用于格式化的 ESLint 流程只负责相应的自动修复;一项可执行检查确保这些可修复规则定义保持一致,而 `max-len` 仅用于验证。自有源码中的抑制指令使用 `oxlint-*` 指令和 `typescript/*` 命名空间,未使用的指令仍作为警告报告;vendor 源码保留其上游指令,因为 Oxlint 会排除 `vendor/**`
Oxlint 的 JavaScript 插件兼容层运行 `@stylistic/eslint-plugin``eslint-plugin-sonarjs`,从而继续强制执行现有的格式和文件内重复逻辑规则。兼容层会报告 `@stylistic` 违规并执行其安全修复;`max-len` 仅用于验证。自有源码中的抑制指令使用 `oxlint-*` 指令和 `typescript/*` 命名空间,未使用的指令仍作为警告报告;vendor 源码保留其上游指令,因为 Oxlint 会排除 `vendor/**`
CI 不恢复或保存 lint 结果缓存。`DSH_OXLINT_THREADS` 使共享运行器将同一上限传给 Oxlint 的 `--threads` 选项和类型感知后端的 `GOMAXPROCS` 环境变量;普通本地运行对两者均采用默认值。Pre-commit 应用仅用于格式化的 ESLint 修复,运行不加载项目的 Oxlint 验证应用原生安全修复,接受仅含已忽略文件的文件选择,并通过 lefthook 重新暂存结果。公共 `lint` 和 CI 会先准备生成的声明,并保留完整的类型感知规则。
CI 不恢复或保存 lint 结果缓存。`DSH_OXLINT_THREADS` 使共享运行器将同一上限传给 Oxlint 的 `--threads` 选项和类型感知后端的 `GOMAXPROCS` 环境变量;普通本地运行对两者均采用默认值。Pre-commit 运行不加载项目的 Oxlint 验证应用带一次有界重试的安全修复,接受仅含已忽略文件的文件选择,并通过 lefthook 重新暂存结果。公共 `lint` 和 CI 会先准备生成的声明,并保留完整的类型感知规则。
## 验证
解决两处分析器差异后,迁移后的配置报告与迁移前一致的自有源码无问题基线:移除了一项冗余测试断言,而 `tsc` 要求的一处结构性类型转换使用了窄范围的 Oxlint 抑制指令。以已删除 ESLint 配置的精确 blob 为基准进行的一次性审核在完成规则名映射后确认:源码为 88 项对 88 项,示例为 87 项对 87 项,测试为 83 项对 83 项。已提交的指纹锁定这些经审核的 Oxlint 规则配置及完整的覆盖结构;它既不执行已删除的配置,也不纳入后续的上游预设变更。对 `typescript-eslint@8.61.0` 的评估还确认,`strictTypeChecked` 并未启用 `@typescript-eslint/no-empty-function`;已删除、仅用于测试的 `off` 条目不起作用。
可执行约定测试要求包、host 和 client 项目产生类型感知诊断,断言 client 专用脚本所用的项目,拒绝未匹配的回退分析,并检验 Stylistic、SonarJS 和 nursery 兼容路径。它们还锁定暂存配置不加载项目的继承行为、未使用抑制指令的报告行为、仅选择已忽略暂存文件的情况、格式化器与验证器之间的规则一致性,以及最终格式化的字节。运行器测试锁定两项工作线程控制,类型检查则确认迁移引发的源码改动没有破坏 TypeScript 程序。
可执行约定测试要求包、host 和 client 项目产生类型感知诊断,断言 client 专用脚本所用的项目,拒绝未匹配的回退分析,并检验 Stylistic、SonarJS 和 nursery 兼容路径。它们还锁定暂存配置不加载项目的继承行为与 TypeGraph fixture 覆盖、未使用抑制指令的报告行为、仅选择已忽略暂存文件的情况、完整的 Stylistic 规则集,以及收敛后最终格式化的字节。运行器测试锁定两项工作线程控制,类型检查则确认迁移引发的源码改动没有破坏 TypeScript 程序。
## 考虑过的替代方案
**在全仓库范围内同时运行两个 linter。** 所有正确性规则均可通过 Oxlint 原生规则、nursery 规则或 JavaScript 插件兼容层获得。在全仓库范围启用 ESLint 回退会保留较慢的项目服务初始化和两套正确性配置,却不会增加任何检查;保留的 ESLint 流程被刻意限制为不加载项目的暂存文件格式化
**在全仓库范围内同时运行两个 linter。** 所有正确性规则均可通过 Oxlint 原生规则、nursery 规则或 JavaScript 插件兼容层获得。在全仓库范围启用 ESLint 回退会保留较慢的项目服务初始化和两套正确性配置,却不会增加任何检查。
**依赖兼容层修复。** 兼容层会报告既有的 `@stylistic` 规则,但在 Oxlint 的两种修复模式下都不会应用这些规则的修复。保留窄范围的暂存文件格式化器,可以在不将 ESLint 扩张回仓库 linter 的情况下维持贡献者约定
**使用单独的格式化器。** 迁移保留窄范围的 ESLint 流程,因为当时认为兼容层无法执行修复。锁定版本的工具链证明能够执行相同修复后,[仅使用 Oxlint 的修复工作流](2026-08-09-oxlint-only-fix-workflow.md)以一次有界重试取代了该部分决策
**移除尚无原生实现的 @stylistic 或 SonarJS 规则。** 这会移除依赖,但也会削弱机械质量约定。兼容层会保留这些规则,直到能够通过单独决策评估原生替代规则。
@@ -42,4 +42,4 @@ CI 不恢复或保存 lint 结果缓存。`DSH_OXLINT_THREADS` 使共享运行
类型感知诊断现在来自通过 `oxlint-tsgolint` 捆绑的 TypeScript Go 分析器,因此即使 `tsc` 接受同一程序,边界场景下的类型推断也可能与 typescript-eslint 不同。lint 与类型检查仍是两项相互独立的必要证据。
JavaScript 插件兼容 API暂存配置和暂存文件格式化器是需要维护的额外边界。每次提交把类型感知诊断留给公共 lint 和 CI,在 Oxlint 之前启动一次不加载项目的 ESLint,并避免依赖生成的声明。根目录开发依赖图仍保留 ESLint 和 TypeScript 解析器。全仓库验证、类型感知分析、缓存政策、工作线程控制和内联指令仍由 Oxlint 负责。
JavaScript 插件兼容 API暂存配置是需要维护的额外边界。每次提交把类型感知诊断留给公共 lint 和 CI,并避免依赖生成的声明。全仓库验证、修复、类型感知分析、缓存政策、工作线程控制和内联指令仍由 Oxlint 负责。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-09-oxlint-only-fix-workflow.md
2026-08-09-oxlint-only-fix-workflow.md: bc38812db101499d980c38cb6c1dc5cba32e5cd6
2026-08-09-oxlint-only-fix-workflow.zh.md: 91674837dd7c0c2da8d5b6214082dc7a2f2f0fac
@@ -0,0 +1,37 @@
# Agent Note: Oxlint-only fix workflow
Status: implemented
English | [中文](2026-08-09-oxlint-only-fix-workflow.zh.md)
## Problem
The [repository linter migration](2026-07-29-oxlint-linter.md) retained a formatting-only ESLint invocation because Oxlint's JavaScript-plugin bridge was treated as validation-only. The pinned Oxlint toolchain executes the safe fixers supplied by `@stylistic/eslint-plugin`, so the separate formatter duplicates a configuration boundary, command startup, and direct `eslint` plus `@typescript-eslint/parser` dependencies.
A single Oxlint invocation is not an equivalent replacement. Overlapping plugin fixes can apply one change while leaving a newly exposed diagnostic; the repository's `semi` and `object-curly-spacing` fixture requires a second pass before it is clean. The workflow must retry that case without printing obsolete first-pass diagnostics.
## Decision
All repository lint and fix workflows invoke Oxlint through [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts). Normal validation remains one process with inherited output. An invocation containing `--fix`, `--fix-suggestions`, or `--fix-dangerously` captures the first Oxlint result; success emits its stdout and stderr on their original channels, while a completed non-zero run discards its potentially obsolete diagnostics and runs the same command once more with inherited output. The runner re-raises a child signal instead of retrying or converting it to an exit code, and the second process completion is final.
The `lint:fix` package script and staged lefthook job use that runner directly. The type-aware root profile still ignores preserved TypeGraph fixture shapes that `oxlint-tsgolint` cannot analyze; the project-free staged profile re-includes that directory, carries its intentional `any` and quote exceptions, and applies its style fixes before the full type-aware fix pass. The formatting-only ESLint configuration and the direct `eslint` and `@typescript-eslint/parser` development dependencies are absent. `@stylistic/eslint-plugin` and `eslint-plugin-sonarjs` remain Oxlint JavaScript plugins because they preserve enforced rules; pnpm still installs ESLint as their declared peer, but no repository configuration or workflow invokes it.
## Verification
The executable lint contract drives a deliberately overlapping style violation through the repository runner and requires a successful exit plus exact final bytes. The same contract pins the complete Stylistic rule set, project-free TypeGraph fixture coverage, the package scripts, the staged hook command, the deleted formatter configuration, and the absence of direct ESLint parser and runner dependencies. Existing executable probes continue to cover the Stylistic and SonarJS compatibility plugins, project-free staged validation, and type-aware project discovery.
## Alternatives considered
**Keep the formatting-only ESLint pass.** This preserves ESLint's built-in multipass behavior but retains a second runner, a duplicated formatting config, and direct dependencies after Oxlint can execute the same plugin fixes.
**Run Oxlint once with `--fix`.** This is simpler, but overlapping safe fixes can leave the command partially formatted and non-zero even though another identical pass completes it.
**Adopt Oxfmt.** A formatter migration changes the repository's output contract and would create an unrelated formatting diff. It remains a separate decision from removing the redundant ESLint execution path.
**Remove the JavaScript compatibility plugins.** This would eliminate their ESLint peer graph but would also drop the enforced Stylistic and SonarJS rules. Dependency-tree purity does not justify weakening the quality contract.
## Consequences
Contributors, package scripts, hooks, and CI have one lint runner and one rule configuration. The staged profile repeats the root ignore list so it can re-include formatter-only fixtures without exposing vendor or generated files. A completed non-zero fix invocation always pays for one retry, including a stable unfixable error. The first pass buffers each output stream up to 64 MiB so obsolete diagnostics are not printed when the retry succeeds; process creation and capture failures, including that limit being exceeded, surface immediately without a retry.
The dependency lock can still contain ESLint through plugin peer resolution. Removing that transitive package requires native replacements or a formatter decision that also replaces the compatibility plugins; it is not part of the workflow simplification.
@@ -0,0 +1,37 @@
# Agent Note: 仅使用 Oxlint 的修复工作流
Status: implemented
[English](2026-08-09-oxlint-only-fix-workflow.md) | 中文
## 问题
[仓库 linter 迁移](2026-07-29-oxlint-linter.md)保留了一次仅用于格式化的 ESLint 调用,因为当时认为 Oxlint 的 JavaScript 插件桥接层只能用于校验。固定版本的 Oxlint 工具链能够执行 `@stylistic/eslint-plugin` 提供的安全修复,因此单独的格式化器重复引入了配置边界、命令启动过程,以及对 `eslint``@typescript-eslint/parser` 的直接依赖。
单次调用 Oxlint 并不能实现等价替代。相互重叠的插件修复可能先应用一项变更,却留下由此新暴露的诊断;仓库中包含 `semi``object-curly-spacing` 违规的 fixture(测试前置数据)需要运行第二轮才能完全修复。工作流必须重试这种情况,同时不得打印第一轮中已经失效的诊断。
## 决策
仓库的所有 lint 与修复工作流都通过 [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts) 调用 Oxlint。普通校验仍由单个进程执行,并直接继承输出。包含 `--fix``--fix-suggestions``--fix-dangerously` 的调用会捕获第一次 Oxlint 的运行结果;如果成功,就通过原有通道输出其 stdout 和 stderr;如果进程正常结束但状态非零,则丢弃其中可能已经失效的诊断,随后以继承输出的方式再运行一次相同命令。子进程被信号终止时,运行器会重新触发该信号,而不会重试或将其转换为退出码;第二个进程的结束结果作为最终结果。
`lint:fix` 包脚本和处理暂存文件的 lefthook 作业直接使用该运行器。类型感知的根配置仍会忽略 `oxlint-tsgolint` 无法分析、需要保留原始形态的 TypeGraph fixture;不加载项目的暂存配置会重新纳入该目录,保留其中有意设置的 `any` 与引号规则例外,并在完整的类型感知修复轮次之前应用其样式修复。仓库中不存在仅用于格式化的 ESLint 配置,也不直接依赖 `eslint``@typescript-eslint/parser` 这两个开发依赖。`@stylistic/eslint-plugin``eslint-plugin-sonarjs` 仍作为 Oxlint 的 JavaScript 插件保留,因为它们承载了已强制执行的规则;pnpm 仍会将 ESLint 作为这些插件声明的对等依赖(peer dependency)进行安装,但仓库中的配置和工作流均不会调用它。
## 验证
可执行 lint 约定将一项特意构造、修复相互重叠的样式违规交给仓库运行器处理,并要求运行器成功退出且最终字节完全一致。同一约定还会固定完整的 Stylistic 规则集、不加载项目时对 TypeGraph fixture 的覆盖、包脚本、暂存文件钩子命令、已删除的格式化器配置,以及 ESLint 解析器和运行器不存在直接依赖这一事实。现有的可执行探针继续覆盖 Stylistic 和 SonarJS 兼容插件、不加载项目的暂存文件校验,以及类型感知的项目发现。
## 考虑过的替代方案
**保留仅用于格式化的 ESLint 轮次。** 这会保留 ESLint 内置的多轮修复行为,但在 Oxlint 已能执行相同插件修复的情况下,仍需维护第二个运行器、重复的格式化配置和直接依赖。
**仅运行一次带 `--fix` 的 Oxlint。** 这样更简单,但相互重叠的安全修复可能使命令只完成部分格式化并以非零状态退出,即使再次运行同一命令就能完成修复。
**采用 Oxfmt。** 迁移格式化器会改变仓库的输出约定,并产生无关的格式化 diff。它与移除冗余 ESLint 执行路径是两项独立决策。
**移除 JavaScript 兼容插件。** 这会消除这些插件引入的 ESLint 对等依赖图,但也会移除强制执行的 Stylistic 和 SonarJS 规则。追求依赖树纯净不能成为削弱质量约定的理由。
## 结果
贡献者、包脚本、钩子和 CI 统一使用一个 lint 运行器和一份规则配置。暂存配置会重复根配置的忽略清单,以便重新纳入仅用于格式化的 fixture,同时避免将 vendor 或生成文件纳入检查。修复进程正常结束但状态非零时,始终会额外执行一次重试;稳定存在且无法修复的错误也不例外。第一轮的每条输出流最多缓冲 64 MiB,因此重试成功时不会打印已经失效的诊断;进程创建或输出捕获失败(包括超出该上限)时会立即报告错误,不会重试。
依赖锁中仍可能因插件的对等依赖解析而包含 ESLint。要移除这个传递依赖,需要使用原生替代方案,或另行决定采用能够同时取代兼容插件的格式化器;这不属于本次工作流简化的范围。
+18
View File
@@ -298,6 +298,24 @@
"jsPlugins": [
"@stylistic/eslint-plugin"
]
},
{
// The project-free staged profile re-includes this syntax-coverage fixture.
"files": [
"packages/typert/generator/tests/fixtures/type-model/**/*.{ts,tsx}"
],
"rules": {
"typescript/no-explicit-any": "off"
}
},
{
// TypeGraph coverage must retain source-authored syntax that the normal quote rule forbids.
"files": [
"packages/typert/generator/tests/fixtures/type-model/packages/host/src/models.ts"
],
"rules": {
"@stylistic/quotes": "off"
}
}
]
}
+16 -1
View File
@@ -3,5 +3,20 @@
"extends": ["./.oxlintrc.json"],
"options": {
"typeAware": false
}
},
"ignorePatterns": [
"**/lib/**",
"**/node_modules/**",
"**/.sessions/**",
".claude/**",
"**/.doc-typecheck-*/**",
"**/.node-next-types-*/**",
"website/.generated/**",
"vendor/**",
"native/**",
"**/*.js",
"**/*.mjs",
"**/*.config.ts",
"packages/client/tsdown.client.ts"
]
}
-2
View File
@@ -139,7 +139,6 @@ External packages **directly declared** only by repository tooling, test infrast
| [`@types/spdx-expression-parse`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@types/turndown`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@types/ws`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint) | MIT |
| [`@vitejs/plugin-react`](https://github.com/vitejs/vite-plugin-react) | MIT |
| [`@vitest/coverage-v8`](https://github.com/vitest-dev/vitest) | MIT |
| [`@yarnpkg/cli-dist`](https://github.com/yarnpkg/berry) | BSD-2-Clause |
@@ -148,7 +147,6 @@ External packages **directly declared** only by repository tooling, test infrast
| [`dayjs`](https://github.com/iamkun/dayjs) | MIT |
| [`debug`](https://github.com/debug-js/debug) | MIT |
| [`esbuild`](https://github.com/evanw/esbuild) | MIT |
| [`eslint`](https://github.com/eslint/eslint) | MIT |
| [`eslint-plugin-sonarjs`](https://github.com/SonarSource/SonarJS) | LGPL-3.0-only |
| [`execa`](https://github.com/sindresorhus/execa) | MIT |
| [`fast-check`](https://github.com/dubzzz/fast-check) | MIT |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/development.md
development.md: f7084403b30e8a6d12c4ef2b886f8fb61ed39b26
development.zh.md: 56df93beddbb128cac8e0bcf2adaa9dcfb72c751
development.md: 8b6c148d87fdd6288695b0029b9dfcc0139d2144
development.zh.md: 55ca013cc0dd901500e340a7447057de27665f61
+1 -1
View File
@@ -106,7 +106,7 @@ The installer probes the exact Node/tsx driver entrypoint before publishing its
lefthook is configured in `lefthook.yml` as a fast local checkpoint:
- `pre-commit` verifies staged pairing records against the staged owner blobs, applies formatting-only ESLint fixes, validates the staged files with the project-free `.oxlintrc.staged.json` profile and applies Oxlint's native fixes, regenerates `THIRD_PARTY_NOTICES.md` when a staged file is one of its inputs, checks the staged diff for whitespace errors, and runs the vendor manifest guard.
- `pre-commit` verifies staged pairing records against the staged owner blobs, validates staged files with the project-free `.oxlintrc.staged.json` profile and applies Oxlint fixes with one bounded retry, regenerates `THIRD_PARTY_NOTICES.md` when a staged file is one of its inputs, checks the staged diff for whitespace errors, and runs the vendor manifest guard.
- `pre-merge-commit` performs the same index-backed pairing check before Git creates an automatic merge commit.
- `pre-push` runs `pnpm run typecheck`, which completes the Host lib phase, including generated TypeRT contracts, before the Client TypeScript check.
+1 -1
View File
@@ -106,7 +106,7 @@ DEEPSEEK_BASE_URL=https://... # optional
lefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点:
- `pre-commit` 对照暂存的配对文档 blob 校验暂存的配对记录,应用仅用于格式化的 ESLint 修复,使用不加载项目的 `.oxlintrc.staged.json` 配置验证暂存文件应用 Oxlint 的原生修复,在暂存文件属于 `THIRD_PARTY_NOTICES.md` 的输入时重新生成该文件,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫;
- `pre-commit` 对照暂存的配对文档 blob 校验暂存的配对记录,使用不加载项目的 `.oxlintrc.staged.json` 配置验证暂存文件,并通过一次有界重试应用 Oxlint 修复,在暂存文件属于 `THIRD_PARTY_NOTICES.md` 的输入时重新生成该文件,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫;
- `pre-merge-commit` 在 Git 创建自动合并提交前执行同样以索引为准的配对检查;
- `pre-push` 运行 `pnpm run typecheck`;该命令会先完成包含 TypeRT 约定生成的完整 Host lib 阶段,再运行 Client TypeScript 检查。
-59
View File
@@ -1,59 +0,0 @@
import stylistic from '@stylistic/eslint-plugin'
import parser from '@typescript-eslint/parser'
// Oxlint's JavaScript-plugin compatibility layer reports these rules but does
// not execute their fixers. Keep this config formatting-only: Oxlint remains
// the authoritative repository linter after this pass applies safe fixes.
export default [
{
ignores: [
'**/lib/**',
'**/node_modules/**',
'**/.sessions/**',
'.claude/**',
'**/.doc-typecheck-*/**',
'**/.node-next-types-*/**',
// Do not mirror Oxlint's contract-fixture ignore: those files must reach this formatter.
'website/.generated/**',
'vendor/**',
'native/**',
'**/*.js',
'**/*.mjs',
'**/*.config.ts',
'packages/client/tsdown.client.ts',
],
},
{
files: ['**/*.{ts,tsx,mts,cts}'],
languageOptions: {
parser,
parserOptions: {
sourceType: 'module',
},
},
plugins: {
'@stylistic': stylistic,
},
rules: {
'@stylistic/indent': ['error', 2],
'@stylistic/semi': ['error', 'never'],
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/eol-last': ['error', 'always'],
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
'@stylistic/member-delimiter-style': ['error', {
multiline: { delimiter: 'none' },
singleline: { delimiter: 'semi', requireLast: false },
}],
},
},
{
// TypeGraph coverage must retain source-authored syntax that the normal quote rule forbids.
files: ['packages/typert/generator/tests/fixtures/type-model/packages/host/src/models.ts'],
rules: {
'@stylistic/quotes': 'off',
},
},
]
-7
View File
@@ -8,13 +8,6 @@ pre-commit:
glob: '*.i18n.yaml'
run: node_modules/.bin/tsx scripts/verify-translation-pairing.ts --cached {staged_files}
- name: format (staged)
glob: '*.{ts,tsx,mts,cts,mjs}'
exclude:
- 'vendor/*/src/**'
run: node_modules/.bin/eslint --config eslint.format.config.mjs --fix --no-warn-ignored {staged_files}
stage_fixed: true
- name: lint (staged)
glob: '*.{ts,tsx,mts,cts,mjs}'
exclude:
+1 -3
View File
@@ -28,7 +28,7 @@
"lint": "npm run build:lib:host && npm run lint:contracts-ready",
"lint:contracts-ready": "tsx scripts/run-oxlint.ts .",
"lint:fix": "npm run build:lib:host && npm run lint:fix:contracts-ready",
"lint:fix:contracts-ready": "eslint --config eslint.format.config.mjs --fix . && tsx scripts/run-oxlint.ts . --fix",
"lint:fix:contracts-ready": "tsx scripts/run-oxlint.ts --config .oxlintrc.staged.json packages/typert/generator/tests/fixtures/type-model --fix && tsx scripts/run-oxlint.ts . --fix",
"duplication": "jscpd --config .jscpd.json packages scripts",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
@@ -139,10 +139,8 @@
"@types/mdast": "^4.0.4",
"@types/node": "^22.20.0",
"@types/spdx-expression-parse": "^4.0.0",
"@typescript-eslint/parser": "8.61.0",
"@vitest/coverage-v8": "^4.1.8",
"@yarnpkg/cli-dist": "4.17.1",
"eslint": "10.5.0",
"eslint-plugin-sonarjs": "^4.1.0",
"execa": "^10.0.0",
"fast-check": "^4.8.0",
-105
View File
@@ -41,18 +41,12 @@ importers:
'@types/spdx-expression-parse':
specifier: ^4.0.0
version: 4.0.0
'@typescript-eslint/parser':
specifier: 8.61.0
version: 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
'@vitest/coverage-v8':
specifier: ^4.1.8
version: 4.1.8(vitest@4.1.8)
'@yarnpkg/cli-dist':
specifier: 4.17.1
version: 4.17.1
eslint:
specifier: 10.5.0
version: 10.5.0(jiti@2.7.0)
eslint-plugin-sonarjs:
specifier: ^4.1.0
version: 4.1.0(eslint@10.5.0(jiti@2.7.0))
@@ -9962,53 +9956,10 @@ packages:
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@typescript-eslint/parser@8.61.0':
resolution: {integrity: sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.61.0':
resolution: {integrity: sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.61.0':
resolution: {integrity: sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.61.0':
resolution: {integrity: sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/tsconfig-utils@8.65.0':
resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.61.0':
resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.65.0':
resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.61.0':
resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.61.0':
resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.3':
resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==}
@@ -15039,64 +14990,8 @@ snapshots:
dependencies:
'@types/node': 22.20.0
'@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.61.0
'@typescript-eslint/types': 8.61.0
'@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.61.0
debug: 4.4.3
eslint: 10.5.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.61.0(typescript@6.0.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3)
'@typescript-eslint/types': 8.65.0
debug: 4.4.3
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.61.0':
dependencies:
'@typescript-eslint/types': 8.61.0
'@typescript-eslint/visitor-keys': 8.61.0
'@typescript-eslint/tsconfig-utils@8.61.0(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
'@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
'@typescript-eslint/types@8.61.0': {}
'@typescript-eslint/types@8.65.0': {}
'@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3)':
dependencies:
'@typescript-eslint/project-service': 8.61.0(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.61.0(typescript@6.0.3)
'@typescript-eslint/types': 8.61.0
'@typescript-eslint/visitor-keys': 8.61.0
debug: 4.4.3
minimatch: 10.2.5
semver: 7.8.4
tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.61.0':
dependencies:
'@typescript-eslint/types': 8.61.0
eslint-visitor-keys: 5.0.1
'@ungap/structured-clone@1.3.3': {}
'@upsetjs/venn.js@2.0.0':
+121 -31
View File
@@ -1,14 +1,15 @@
import { spawnSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { existsSync } from 'node:fs'
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { join, relative } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { fileURLToPath } from 'node:url'
import { flattenDiagnosticMessageText, parseConfigFileTextToJson } from 'typescript'
import { describe, expect, it } from 'vitest'
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
const eslintCli = fileURLToPath(new URL('../node_modules/eslint/bin/eslint.js', import.meta.url))
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
const tsxCli = fileURLToPath(new URL('../node_modules/tsx/dist/cli.mjs', import.meta.url))
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
@@ -18,11 +19,11 @@ function isUnknownArray(value: unknown): value is unknown[] {
return Array.isArray(value)
}
function runStagedFormatter(paths: readonly string[]) {
return spawnSync(process.execPath, [eslintCli, '--config', 'eslint.format.config.mjs', '--fix', '--no-warn-ignored', ...paths], {
function runRepositoryOxlint(args: readonly string[], env: NodeJS.ProcessEnv = {}) {
return spawnSync(process.execPath, [tsxCli, 'scripts/run-oxlint.ts', ...args], {
cwd: repositoryRoot,
encoding: 'utf8',
env: { ...process.env, NO_COLOR: '1' },
env: { ...process.env, NO_COLOR: '1', ...env },
})
}
@@ -150,7 +151,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
}
}, 20_000)
it('keeps formatter rules aligned with Oxlint validation', async () => {
it('keeps the complete stylistic contract in Oxlint', async () => {
const oxlintPath = join(repositoryRoot, '.oxlintrc.json')
const result = parseConfigFileTextToJson(oxlintPath, await readFile(oxlintPath, 'utf8'))
if (result.error !== undefined) {
@@ -160,27 +161,67 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
if (!isRecord(parsed) || !isUnknownArray(parsed.overrides)) {
throw new Error('.oxlintrc.json must contain an overrides array')
}
expect(parsed.ignorePatterns).toEqual(expect.arrayContaining([
'packages/typert/generator/tests/fixtures/type-model/**',
]))
const stylisticOverride = parsed.overrides.find((value: unknown) =>
isRecord(value) && isRecord(value.rules) && '@stylistic/max-len' in value.rules)
if (!isRecord(stylisticOverride) || !isRecord(stylisticOverride.rules)) {
throw new Error('.oxlintrc.json must contain the @stylistic validator override')
}
const validatorRules = { ...stylisticOverride.rules }
const maxLen = validatorRules['@stylistic/max-len']
delete validatorRules['@stylistic/max-len']
expect(stylisticOverride.rules).toMatchObject({
'@stylistic/indent': ['error', 2],
'@stylistic/semi': ['error', 'never'],
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/eol-last': ['error', 'always'],
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
'@stylistic/member-delimiter-style': ['error', {
multiline: { delimiter: 'none' },
singleline: { delimiter: 'semi', requireLast: false },
}],
'@stylistic/max-len': ['error', { code: 140, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true }],
})
const typeGraphOverride = parsed.overrides.find((value: unknown) =>
isRecord(value)
&& isUnknownArray(value.files)
&& value.files.includes('packages/typert/generator/tests/fixtures/type-model/packages/host/src/models.ts'))
expect(typeGraphOverride).toMatchObject({
rules: { '@stylistic/quotes': 'off' },
})
})
const formatterUrl = pathToFileURL(join(repositoryRoot, 'eslint.format.config.mjs')).href
const formatterModule = await import(formatterUrl) as unknown
if (!isRecord(formatterModule) || !isUnknownArray(formatterModule.default)) {
throw new Error('eslint.format.config.mjs must default-export a config array')
}
const formatterOverride = formatterModule.default.find((value: unknown) => isRecord(value) && isRecord(value.rules))
if (!isRecord(formatterOverride) || !isRecord(formatterOverride.rules)) {
throw new Error('eslint.format.config.mjs must contain a rules object')
it('checks preserved TypeGraph syntax without type-aware analysis', () => {
const result = runOxlint([
'--config',
'.oxlintrc.staged.json',
'packages/typert/generator/tests/fixtures/type-model',
])
expect(result.error).toBeUndefined()
expect(result.status, normalizedOutput(result)).toBe(0)
})
it('keeps repository lint workflows Oxlint-only', async () => {
const packageJson = JSON.parse(await readFile(join(repositoryRoot, 'package.json'), 'utf8')) as unknown
if (!isRecord(packageJson) || !isRecord(packageJson.scripts) || !isRecord(packageJson.devDependencies)) {
throw new Error('package.json must contain scripts and devDependencies objects')
}
expect(validatorRules).toStrictEqual(formatterOverride.rules)
expect(maxLen).toStrictEqual(['error', { code: 140, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true }])
expect(packageJson.scripts['lint:contracts-ready']).toBe('tsx scripts/run-oxlint.ts .')
expect(packageJson.scripts['lint:fix:contracts-ready']).toBe(
'tsx scripts/run-oxlint.ts --config .oxlintrc.staged.json packages/typert/generator/tests/fixtures/type-model --fix && tsx scripts/run-oxlint.ts . --fix',
)
expect(packageJson.devDependencies).not.toHaveProperty('eslint')
expect(packageJson.devDependencies).not.toHaveProperty('@typescript-eslint/parser')
expect(existsSync(join(repositoryRoot, 'eslint.format.config.mjs'))).toBe(false)
const lefthook = await readFile(join(repositoryRoot, 'lefthook.yml'), 'utf8')
expect(lefthook).toContain('scripts/run-oxlint.ts --config .oxlintrc.staged.json --fix')
expect(lefthook).not.toContain('node_modules/.bin/eslint')
expect(lefthook).not.toContain('eslint.format.config.mjs')
})
it('reports an unused suppression', async () => {
@@ -227,10 +268,13 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
if (result.error !== undefined) {
throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
}
expect(result.config).toMatchObject({
const stagedConfig = result.config as unknown
if (!isRecord(stagedConfig)) throw new Error('.oxlintrc.staged.json must contain a config object')
expect(stagedConfig).toMatchObject({
extends: ['./.oxlintrc.json'],
options: { typeAware: false },
})
expect(stagedConfig.ignorePatterns).not.toContain('packages/typert/generator/tests/fixtures/type-model/**')
const suffix = randomUUID()
const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
@@ -254,9 +298,58 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
}
})
it('applies staged stylistic fixes before Oxlint validation', async () => {
it('preserves successful fix output channels', async () => {
const suffix = randomUUID()
const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
try {
await writeFile(path, '// oxlint-disable-next-line no-console\nexport const value = 1\n')
const result = runRepositoryOxlint([
'--config',
'.oxlintrc.staged.json',
'--format',
'unix',
'--fix',
relative(repositoryRoot, path),
])
expect(result.error).toBeUndefined()
expect(result.status, normalizedOutput(result)).toBe(0)
expect(result.stdout).toContain('Unused oxlint-disable directive')
expect(result.stderr).toBe('')
} finally {
await rm(path, { force: true })
}
})
it('prints only the final diagnostics when a fix retry still fails', async () => {
const suffix = randomUUID()
const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
try {
await writeFile(path, `export const longProbe = ${'1 + '.repeat(80)}1\n`)
const result = runRepositoryOxlint([
'--config',
'.oxlintrc.staged.json',
'--format',
'unix',
'--fix',
relative(repositoryRoot, path),
])
const output = normalizedOutput(result)
expect(result.error).toBeUndefined()
expect(result.status, output).toBe(1)
expect(output.match(/@stylistic\(max-len\)/g)).toHaveLength(1)
} finally {
await rm(path, { force: true })
}
})
it.each(['--fix', '--fix-suggestions', '--fix-dangerously'])(
'converges overlapping staged stylistic fixes through Oxlint under %s',
async (fixFlag) => {
const suffix = randomUUID()
const configPath = await writeContractConfig(suffix)
const directory = join(repositoryRoot, 'scripts', `.oxlint-contract-${suffix}`)
const path = join(directory, 'fix.ts')
@@ -265,19 +358,16 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
await writeFile(path, 'const value={answer:1}; \nconsole.log(value)\n')
const relativePath = relative(repositoryRoot, path)
const formatResult = runStagedFormatter([relativePath])
const lintResult = runOxlint(['--config', relative(repositoryRoot, configPath), '--fix', relativePath])
const lintResult = runRepositoryOxlint(['--config', '.oxlintrc.staged.json', fixFlag, relativePath])
expect(formatResult.error).toBeUndefined()
expect(formatResult.status, normalizedOutput(formatResult)).toBe(0)
expect(lintResult.error).toBeUndefined()
expect(lintResult.status, normalizedOutput(lintResult)).toBe(0)
expect(normalizedOutput(lintResult)).not.toContain('@stylistic')
await expect(readFile(path, 'utf8')).resolves.toBe('const value={ answer:1 }\nconsole.log(value)\n')
} finally {
await Promise.all([
rm(directory, { recursive: true, force: true }),
rm(configPath, { force: true }),
])
await rm(directory, { recursive: true, force: true })
}
}, 20_000)
},
20_000,
)
})
+43 -1
View File
@@ -3,6 +3,12 @@ import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
const MAX_CAPTURED_OUTPUT_BYTES = 64 * 1024 * 1024
const FIX_FLAGS = new Set(['--fix', '--fix-dangerously', '--fix-suggestions'])
function isFixInvocation(args: readonly string[]): boolean {
return args.some(arg => FIX_FLAGS.has(arg))
}
/** Complete Oxlint child-process arguments and environment. */
export interface OxlintInvocation {
@@ -32,14 +38,50 @@ export function resolveOxlintInvocation(args: readonly string[], env: NodeJS.Pro
}
}
function completeFrom(result: { readonly signal: NodeJS.Signals | null; readonly status: number | null }): void {
if (result.signal !== null) {
process.kill(process.pid, result.signal)
return
}
process.exitCode = result.status ?? 1
}
function main(): void {
const invocation = resolveOxlintInvocation(process.argv.slice(2), process.env)
if (!isFixInvocation(invocation.args)) {
const result = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
env: invocation.env,
stdio: 'inherit',
})
if (result.error !== undefined) throw result.error
process.exitCode = result.status ?? 1
completeFrom(result)
return
}
const first = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
encoding: 'utf8',
env: invocation.env,
maxBuffer: MAX_CAPTURED_OUTPUT_BYTES,
})
if (first.error !== undefined) throw first.error
if (first.signal !== null) {
completeFrom(first)
return
}
if (first.status === 0) {
process.stdout.write(first.stdout)
process.stderr.write(first.stderr)
process.exitCode = 0
return
}
// Overlapping JS-plugin fixes can expose one more fixable diagnostic after the first pass.
const second = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
env: invocation.env,
stdio: 'inherit',
})
if (second.error !== undefined) throw second.error
completeFrom(second)
}
const entrypoint = process.argv[1]