diff --git a/.agents/notes/implemented/process/2026-07-29-oxlint-linter.i18n.yaml b/.agents/notes/implemented/process/2026-07-29-oxlint-linter.i18n.yaml index 9f9def3bdc..de6568776b 100644 --- a/.agents/notes/implemented/process/2026-07-29-oxlint-linter.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-29-oxlint-linter.i18n.yaml @@ -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: 5bf7bd417860b1bd9eb9fd421d871fbc4d5256ea -2026-07-29-oxlint-linter.zh.md: 56bdbdb598f13bf69049dc818fb5129fa6a987e2 +2026-07-29-oxlint-linter.md: 6a6b76d3d53ecdd065c9c43c0f60644ba473672c +2026-07-29-oxlint-linter.zh.md: f534d6914ca1ba6bb9292d9f102e329d6adbc2c5 diff --git a/.agents/notes/implemented/process/2026-07-29-oxlint-linter.md b/.agents/notes/implemented/process/2026-07-29-oxlint-linter.md index 5bf7bd4178..6a6b76d3d5 100644 --- a/.agents/notes/implemented/process/2026-07-29-oxlint-linter.md +++ b/.agents/notes/implemented/process/2026-07-29-oxlint-linter.md @@ -22,7 +22,7 @@ CI does not restore or save a lint-result cache. `DSH_OXLINT_THREADS` optionally ## 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. The repository lint command exercises type-aware rules and both JavaScript compatibility plugins. Focused gate-scheduler execution covers the explicit thread-bound path, while typecheck confirms that migration-driven source edits preserve the TypeScript programs. +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 committed fingerprint test normalizes severities and rule-name translations, then deep-compares every active rule and option against the exact deleted ESLint configuration blob: source is 88-to-88, examples are 87-to-87, and tests are 83-to-83, with no missing, extra, or changed pairs. The repository lint command exercises type-aware rules and both JavaScript compatibility plugins. Focused gate-scheduler execution covers the explicit thread-bound path, while typecheck confirms that migration-driven source edits preserve the TypeScript programs. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-29-oxlint-linter.zh.md b/.agents/notes/implemented/process/2026-07-29-oxlint-linter.zh.md index 56bdbdb598..f534d6914c 100644 --- a/.agents/notes/implemented/process/2026-07-29-oxlint-linter.zh.md +++ b/.agents/notes/implemented/process/2026-07-29-oxlint-linter.zh.md @@ -22,7 +22,7 @@ CI 不恢复或保存 lint 结果缓存。`DSH_OXLINT_THREADS` 可以在门禁 ## 验证 -解决两处分析器差异后,迁移后的配置报告与迁移前一致的自有源码无问题基线:移除了一项冗余测试断言,而 `tsc` 要求的一处结构性类型转换使用了窄范围的 Oxlint 抑制指令。仓库 lint 命令会运行类型感知规则和两个 JavaScript 兼容插件。针对门禁调度器的聚焦执行覆盖显式线程上限路径,类型检查则确认迁移引发的源码改动没有破坏 TypeScript 程序。 +解决两处分析器差异后,迁移后的配置报告与迁移前一致的自有源码无问题基线:移除了一项冗余测试断言,而 `tsc` 要求的一处结构性类型转换使用了窄范围的 Oxlint 抑制指令。一项已提交的指纹测试会对严重级别和规则名映射进行归一化,再以已删除 ESLint 配置的精确 blob 为基准,对每一项启用的规则及其选项进行深度比较:源码为 88 项对 88 项,示例为 87 项对 87 项,测试为 83 项对 83 项,不存在缺失、多余或发生变化的配对。仓库 lint 命令会运行类型感知规则和两个 JavaScript 兼容插件。针对门禁调度器的聚焦执行覆盖显式线程上限路径,类型检查则确认迁移引发的源码改动没有破坏 TypeScript 程序。 ## 考虑过的替代方案 diff --git a/scripts/lint-rule-parity.spec.ts b/scripts/lint-rule-parity.spec.ts new file mode 100644 index 0000000000..1ce615edef --- /dev/null +++ b/scripts/lint-rule-parity.spec.ts @@ -0,0 +1,87 @@ +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +type Rules = Record + +interface Profile { + readonly count: number + readonly indexes: readonly number[] + readonly sha256: string +} + +// Captured from eslint.config.mjs blob 696b08282885296830189fdafe7051a356806fc2 +// after mapping @typescript-eslint/* to typescript/* and the five extension +// rules to their Oxlint core equivalents. +const profiles = { + source: { + count: 88, + indexes: [0, 3, 4], + sha256: 'da1dfd77cb6eb66be93d8d3820f9b9b68b7aa391c24680f8851c0910298f9e3b', + }, + example: { + count: 87, + indexes: [0, 1, 3, 4], + sha256: '6a2606053bc1ec1de3b02611de88ea51d201dac13a1f193e4934d33c08b95f08', + }, + test: { + count: 83, + indexes: [2, 3, 4], + sha256: '7995e14926a36c40bd65c474637735222a95fb030395681685f03060e50a7b78', + }, +} as const satisfies Record + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function isUnknownArray(value: unknown): value is unknown[] { + return Array.isArray(value) +} + +function severity(value: unknown): 0 | 1 | 2 { + const level = isUnknownArray(value) ? value[0] : value + if (level === 'off' || level === 0) return 0 + if (level === 'warn' || level === 'warning' || level === 1) return 1 + if (level === 'error' || level === 2) return 2 + throw new Error(`unsupported lint severity: ${JSON.stringify(level)}`) +} + +function normalizedRules(rules: Rules): Rules { + return Object.fromEntries(Object.entries(rules) + .filter(([, value]) => severity(value) > 0) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([name, value]) => { + const options = isUnknownArray(value) ? value.slice(1) : [] + return [name, [severity(value), ...options]] + })) +} + +function mergedRules(config: unknown, indexes: readonly number[]): Rules { + if (!isRecord(config) || !Array.isArray(config.overrides)) { + throw new Error('.oxlintrc.json must contain an overrides array') + } + const merged: Rules = {} + for (const index of indexes) { + const override: unknown = config.overrides[index] + if (!isRecord(override) || !isRecord(override.rules)) { + throw new Error(`.oxlintrc.json override ${index} must contain a rules object`) + } + Object.assign(merged, override.rules) + } + return normalizedRules(merged) +} + +describe('Oxlint migration rule parity', () => { + const path = fileURLToPath(new URL('../.oxlintrc.json', import.meta.url)) + const parsed: unknown = JSON.parse(readFileSync(path, 'utf8')) + + it.each(Object.entries(profiles))('matches the ESLint %s profile pairwise', (_name, profile) => { + const rules = mergedRules(parsed, profile.indexes) + const fingerprint = createHash('sha256').update(JSON.stringify(rules)).digest('hex') + + expect(Object.keys(rules)).toHaveLength(profile.count) + expect(fingerprint).toBe(profile.sha256) + }) +})