From 38791ef0a04f0758e9156edce39ba3f93d302e91 Mon Sep 17 00:00:00 2001 From: norvalbv Date: Tue, 18 Aug 2026 16:57:42 +0100 Subject: [PATCH 1/3] feat(lint): migrate Devkit JS/TS lint to Oxlint --- .github/workflows/gate.yml | 2 +- biome.jsonc | 8 + biome/non-js.jsonc | 9 + biome/regex.jsonc | 30 +++ cli/__tests__/lint-policy.test.mts | 77 +++++++ cli/__tests__/self-host.test.mts | 17 +- dist/biome/non-js.jsonc | 9 + dist/biome/regex.jsonc | 30 +++ dist/oxc/oxlint.devkit-lint.json | 31 +++ dist/package.json | 5 +- .../README.md | 105 +++++++++ .../benchmark.mjs | 204 ++++++++++++++++++ .../results.json | 65 ++++++ docs/decisions/oxc-toolchain-migration.md | 1 + oxc/oxlint.devkit-lint.json | 31 +++ package.json | 5 +- 16 files changed, 618 insertions(+), 11 deletions(-) create mode 100644 biome/non-js.jsonc create mode 100644 biome/regex.jsonc create mode 100644 cli/__tests__/lint-policy.test.mts create mode 100644 dist/biome/non-js.jsonc create mode 100644 dist/biome/regex.jsonc create mode 100644 dist/oxc/oxlint.devkit-lint.json create mode 100644 docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md create mode 100644 docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/benchmark.mjs create mode 100644 docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json create mode 100644 oxc/oxlint.devkit-lint.json diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 0f220f86..7528e921 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -26,7 +26,7 @@ jobs: - name: Format (Oxfmt) run: bun run format:check - - name: Lint and assist (Biome) + - name: Lint and assist (Oxlint + Biome retained lanes) run: bun run lint - name: Anti-slop (Oxlint, baseline-aware) diff --git a/biome.jsonc b/biome.jsonc index 66dd2417..05ad7d3c 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -36,6 +36,14 @@ "!!dist" ] }, + // Native Oxlint owns Devkit's general JS/TS lint policy. Biome keeps its editor assist config + // plus JSON/CSS diagnostics; lint:regex invokes its one JS-only retained rule from + // biome/regex.jsonc. + "javascript": { + "linter": { + "enabled": false + } + }, "overrides": [ { // The gate-engine bins are CLIs — console output is their legitimate UX, not a diff --git a/biome/non-js.jsonc b/biome/non-js.jsonc new file mode 100644 index 00000000..6c91a2f9 --- /dev/null +++ b/biome/non-js.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "../node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../biome.jsonc"], + // Biome stays responsible for JSON/CSS diagnostics. The negative extension + // patterns keep its residual gate from parsing JS/TS after Oxlint takes it. + "files": { + "includes": ["!**/*.js", "!**/*.jsx", "!**/*.mjs", "!**/*.ts", "!**/*.tsx", "!**/*.mts"] + } +} diff --git a/biome/regex.jsonc b/biome/regex.jsonc new file mode 100644 index 00000000..5a68c39e --- /dev/null +++ b/biome/regex.jsonc @@ -0,0 +1,30 @@ +{ + "$schema": "../node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../biome.jsonc"], + // Tests intentionally use inline regexes in assertions, and the former Biome + // profile explicitly exempted them. Keep that policy while the dedicated + // profile checks the production paths. + "files": { + "includes": ["!**/*.test.mts"] + }, + "javascript": { + "linter": { + "enabled": true + } + }, + "linter": { + "rules": { + // This is a deliberately single-rule profile. It avoids re-running the + // JS/TS policy that Oxlint owns, while retaining the one Biome-only rule. + "recommended": false, + "correctness": { + "noUnusedImports": "off", + "noUnusedVariables": "off", + "useHookAtTopLevel": "off" + }, + "performance": { + "useTopLevelRegex": "error" + } + } + } +} diff --git a/cli/__tests__/lint-policy.test.mts b/cli/__tests__/lint-policy.test.mts new file mode 100644 index 00000000..0585123a --- /dev/null +++ b/cli/__tests__/lint-policy.test.mts @@ -0,0 +1,77 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterEach, describe, expect, it } from 'vitest'; +import { testSpawnSync as spawnSync } from './_helpers.mts'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const OXLINT = join(ROOT, 'node_modules', '.bin', 'oxlint'); +const OXLINT_CONFIG = join(ROOT, 'oxc', 'oxlint.devkit-lint.json'); +const BIOME = join(ROOT, 'node_modules', '.bin', 'biome'); +const NON_JS_CONFIG = join(ROOT, 'biome', 'non-js.jsonc'); +const REGEX_CONFIG = join(ROOT, 'biome', 'regex.jsonc'); +const roots: string[] = []; + +function fixture(name: string, source: string) { + const root = mkdtempSync(join(tmpdir(), 'devkit-lint-policy-')); + roots.push(root); + const path = join(root, name); + writeFileSync(path, source); + return path; +} + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +describe('Devkit lint ownership', () => { + it('makes the native Oxlint profile fail on a JS/TS correctness violation', () => { + const path = fixture('unused.mts', 'const unused = 1;\n'); + + const result = spawnSync(OXLINT, ['--config', OXLINT_CONFIG, '--disable-nested-config', path], { + encoding: 'utf8', + }); + + expect(result.status).toBe(1); + }); + + it('keeps the existing production-only top-level-regex rule in Biome', () => { + const result = spawnSync( + BIOME, + [ + 'lint', + '--config-path', + REGEX_CONFIG, + '--diagnostic-level=error', + '--stdin-file-path', + 'cli/production.mts', + ], + { encoding: 'utf8', input: 'const matcher = /value/;\nmatcher.test("x");\n' }, + ); + + expect(result.status).toBe(1); + }); + + it.each([ + ['JSON duplicate keys', 'package.json', '{"name":"one","name":"two"}\n'], + ['CSS unknown properties', 'cli/style.css', 'a { colr: red; }\n'], + ])('retains Biome for %s', (_name, path, source) => { + const result = spawnSync( + BIOME, + [ + 'check', + '--config-path', + NON_JS_CONFIG, + '--formatter-enabled=false', + '--assist-enabled=false', + '--error-on-warnings', + '--stdin-file-path', + path, + ], + { encoding: 'utf8', input: source }, + ); + + expect(result.status).toBe(1); + }); +}); diff --git a/cli/__tests__/self-host.test.mts b/cli/__tests__/self-host.test.mts index 9f681a8e..034708fb 100644 --- a/cli/__tests__/self-host.test.mts +++ b/cli/__tests__/self-host.test.mts @@ -218,18 +218,19 @@ describe('buildSelfHostHook', () => { expect(() => execFileSync('sh', ['-c', fragment ?? 'exit 1'], { cwd: root })).toThrow(); }); - // The `--extra` above is only as hard as the script it names, and biome exits 0 when every - // diagnostic is warn-severity. A bare `biome check .` therefore PRINTS its findings into the gate - // log — indistinguishable from a real failure to the reader — and passes the commit anyway (a - // v0.50.0 ship shipped with six of them). `--error-on-warnings` is what makes the exit code match - // what the log shows. devkit's own root config turns `noConsole` (the one deliberately-advisory - // rule in biome/base.jsonc) off for the whole authored surface, so nothing advisory is caught here. - it('backs the lint extra with a biome invocation that exits non-zero on WARNINGS too', () => { + it('backs the lint extra with the native Oxlint policy and fail-closed retained Biome lanes', () => { const pkg: { scripts?: Record } = JSON.parse( readFileSync(join(ROOT, 'package.json'), 'utf8'), ); expect(SELF_HOST_EXTRAS).toContainEqual({ label: 'lint', cmd: 'bun run lint' }); - expect(pkg.scripts?.lint).toBe('biome check --formatter-enabled=false --error-on-warnings .'); + expect(pkg.scripts?.lint).toBe( + 'bun run lint:oxlint && bun run lint:biome && bun run lint:regex', + ); + expect(pkg.scripts?.['lint:oxlint']).toContain('--deny-warnings'); + expect(pkg.scripts?.['lint:biome']).toContain('--config-path biome/non-js.jsonc'); + expect(pkg.scripts?.['lint:biome']).toContain('--assist-enabled=false'); + expect(pkg.scripts?.['lint:biome']).toContain('--error-on-warnings'); + expect(pkg.scripts?.['lint:regex']).toContain('--diagnostic-level=error'); }); it('preserves the advisory fallow-audit gate INSIDE the block (never blocks, survives re-run)', () => { diff --git a/dist/biome/non-js.jsonc b/dist/biome/non-js.jsonc new file mode 100644 index 00000000..6c91a2f9 --- /dev/null +++ b/dist/biome/non-js.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "../node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../biome.jsonc"], + // Biome stays responsible for JSON/CSS diagnostics. The negative extension + // patterns keep its residual gate from parsing JS/TS after Oxlint takes it. + "files": { + "includes": ["!**/*.js", "!**/*.jsx", "!**/*.mjs", "!**/*.ts", "!**/*.tsx", "!**/*.mts"] + } +} diff --git a/dist/biome/regex.jsonc b/dist/biome/regex.jsonc new file mode 100644 index 00000000..5a68c39e --- /dev/null +++ b/dist/biome/regex.jsonc @@ -0,0 +1,30 @@ +{ + "$schema": "../node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../biome.jsonc"], + // Tests intentionally use inline regexes in assertions, and the former Biome + // profile explicitly exempted them. Keep that policy while the dedicated + // profile checks the production paths. + "files": { + "includes": ["!**/*.test.mts"] + }, + "javascript": { + "linter": { + "enabled": true + } + }, + "linter": { + "rules": { + // This is a deliberately single-rule profile. It avoids re-running the + // JS/TS policy that Oxlint owns, while retaining the one Biome-only rule. + "recommended": false, + "correctness": { + "noUnusedImports": "off", + "noUnusedVariables": "off", + "useHookAtTopLevel": "off" + }, + "performance": { + "useTopLevelRegex": "error" + } + } + } +} diff --git a/dist/oxc/oxlint.devkit-lint.json b/dist/oxc/oxlint.devkit-lint.json new file mode 100644 index 00000000..c033fa24 --- /dev/null +++ b/dist/oxc/oxlint.devkit-lint.json @@ -0,0 +1,31 @@ +{ + "$schema": "../node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "oxc", "react"], + "categories": { + "correctness": "deny", + "perf": "deny", + "suspicious": "deny", + "pedantic": "allow", + "restriction": "allow", + "style": "allow", + "nursery": "allow" + }, + "rules": { + "eslint/no-await-in-loop": "allow", + "eslint/no-console": "allow", + "eslint/no-extend-native": "allow", + "eslint/no-shadow": "allow", + "eslint/no-underscore-dangle": "allow", + "eslint/preserve-caught-error": "allow", + "oxc/no-map-spread": "allow", + "react/rules-of-hooks": "deny" + }, + "overrides": [ + { + "files": ["**/upstream-sync/scripts/sync.mjs"], + "rules": { + "eslint/no-unused-vars": "allow" + } + } + ] +} diff --git a/dist/package.json b/dist/package.json index 6bc592bb..89570845 100644 --- a/dist/package.json +++ b/dist/package.json @@ -59,7 +59,10 @@ "test:run": "vitest run", "test:e2e": "vitest run -c vitest.e2e.config.mjs", "playground": "bun scripts/playground.mts", - "lint": "biome check --formatter-enabled=false --error-on-warnings .", + "lint": "bun run lint:oxlint && bun run lint:biome && bun run lint:regex", + "lint:oxlint": "oxlint --config oxc/oxlint.devkit-lint.json --disable-nested-config --deny-warnings cli gate-engine *.mjs skills", + "lint:biome": "biome check --config-path biome/non-js.jsonc --formatter-enabled=false --assist-enabled=false --error-on-warnings .", + "lint:regex": "biome lint --config-path biome/regex.jsonc --diagnostic-level=error cli gate-engine", "lint:anti-slop": "node cli/index.mts anti-slop check", "lint:structure": "eslint cli gate-engine", "benchmarks:check": "bun gate-engine/eval/cli.mts check", diff --git a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md new file mode 100644 index 00000000..f24b6975 --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md @@ -0,0 +1,105 @@ +# Devkit JS/TS lint migration to Oxlint — 2026-08-18 + +**Shortcut:** sc-1787 +**Decision:** move Devkit's own ordinary JavaScript and TypeScript linting from Biome to a +small, explicit native Oxlint policy. Keep Biome only where Oxlint is not the owner: JSON/CSS +diagnostics and one named regex-performance rule. This is a one-off adoption measurement, not a +new ongoing benchmark gate. + +## In plain English + +Before this change, `bun run lint` asked Biome to inspect almost all Devkit source code in one +large pass. After it, Oxlint handles normal JavaScript/TypeScript code problems, and the much +smaller Biome passes handle the few things Oxlint does not own. + +On the same machine and source tree, the middle result of ten runs was: + +| What matters | Before | After | Change | +| --- | ---: | ---: | ---: | +| CPU time used | 2.34 seconds | 0.70 seconds | **70.2% less** | +| Time waited for the command | 0.42 seconds | 0.26 seconds | **36.7% less** | +| Highest combined memory use | 206 MiB | 128 MiB | 37.8% less | + +CPU is the decision metric: less CPU leaves more capacity for local tools and coding agents. The +memory number is recorded as a safety check, not as the reason to migrate. + +## What owns what after the change + +| Concern | Owner | Why | +| --- | --- | --- | +| Normal JS/TS correctness, suspicious-code, and performance checks | Native Oxlint | The explicit 155-rule profile is fast and has no existing Devkit findings. | +| React hook placement | Native Oxlint `react/rules-of-hooks` | This is a direct named replacement for the former React-hook check. | +| Regex literals repeatedly created in production code | Biome `performance/useTopLevelRegex` | Oxlint 1.78.0 has no native equivalent. Tests remain exempt, exactly as before. | +| JSON and CSS diagnostics | Biome | Oxlint does not lint those file types. | +| Formatting | Oxfmt | Already adopted in the earlier formatter decision. | +| Import organisation | Oxfmt/IDE, not this lint gate | Oxfmt can sort imports, but its sort order differs from the old Biome action and enabling it would reformat 412 files. Keep the existing editor assist available and make a focused formatting decision before enforcing Oxfmt sorting. | +| File/folder topology and import walls | ESLint + `eslint-plugin-project-structure` | These are cross-file filesystem rules, not ordinary source lint. | +| Anti-slop debt | Oxlint's vendored JS plugin, through `anti-slop check` | It has its own baseline and staged-index semantics. | +| Type errors | `tsc --noEmit` | Oxlint is not being used as Devkit's type checker. | + +The shipped `biome/base` and `biome/react` presets are deliberately unchanged. They are consumer +contracts, and consumer migration needs its own parity and performance work. This change is only +about Devkit linting Devkit. + +## Policy change, made explicit + +This is not a claim that every rule in Biome's broad `recommended` preset has the same rule name or +message in Oxlint. There is no trustworthy automatic Biome-to-Oxlint translation. Instead, this +change replaces the implicit ambient preset with an explicit Devkit policy: + +- enable Oxlint's native `correctness`, `suspicious`, and `perf` categories plus React hooks; +- deliberately leave `style`, `pedantic`, `restriction`, and `nursery` off, because enabling a + broad style category created 40,755 existing findings rather than a practical gate; +- keep seven native checks off where Devkit's established source conventions legitimately use + them: `no-await-in-loop`, `no-console`, `no-extend-native`, `no-shadow`, + `no-underscore-dangle`, `preserve-caught-error`, and `no-map-spread`; +- keep one file-specific `no-unused-vars` exception for the pre-existing unused helper in + `skills/upstream-sync/scripts/sync.mjs`, rather than pretending this migration has repaired + unrelated code; and +- retain the named Biome-only regex, JSON, and CSS responsibilities above. + +That makes future additions and removals reviewable in `oxc/oxlint.devkit-lint.json` rather than +being hidden inside a changing recommended preset. + +## Why the Biome part is now small + +Running a general `biome check` after Oxlint was counterproductive: even with JavaScript lint +disabled, it still parsed the JS/TS tree for its import-organising assist and made the combined +command slower than the old one. `biome/non-js.jsonc` prevents that duplicate JS/TS work. The +remaining Biome command checks the 29 in-scope JSON/CSS files; `biome/regex.jsonc` checks the one +retained regex rule on 324 production JS/TS files. + +The import organiser remains enabled in the shared Biome presets for editor users. It is simply no +longer a separate CI/static-lint pass for Devkit itself. Oxfmt's built-in import sorter is available +for a future deliberate format migration, not silently enabled here. + +## Measurement protocol + +The control is a clean archive of `origin/main` at `8f562e02c7bef763b2aee1903040d05c67e22f70`, +running its original `bun run lint`. The candidate is this worktree running the proposed command of +the same name. Both therefore include their real process startup and all their retained checks. + +The machine was macOS arm64 with Node 24.19.0, Bun 1.3.1, and ten logical CPUs. There were three +warm-up runs, then ten measured pairs in alternating order. CPU is user plus system time collected +by `/usr/bin/time -lp`; the memory figure samples the command and its children every 10 ms. Raw +samples and summaries are in [results.json](results.json); the reproducer is +[benchmark.mjs](benchmark.mjs). + +The p95 (the slowest of these ten runs) also improved: CPU fell from 2.57 seconds to 0.73 seconds, +and wall time from 0.52 seconds to 0.30 seconds. + +## Acceptance checks + +- The full proposed `bun run lint` exits clean. +- A fixture proves the native Oxlint profile rejects an unused JS/TS value. +- A fixture proves the retained Biome profile still rejects a regex literal in production code. +- The native runner uses a separate config with `--disable-nested-config`, so anti-slop's baseline + config cannot accidentally join the ordinary lint pass. +- The prior 412-file Oxfmt import-sort rewrite is intentionally not part of this PR. + +## Sources + +- [Oxlint configuration](https://oxc.rs/docs/guide/usage/linter/config.html) +- [Oxlint rules and native plugins](https://oxc.rs/docs/guide/usage/linter/rules.html) +- [Oxfmt import sorting](https://oxc.rs/docs/guide/usage/formatter/sorting.html) +- [Earlier retain experiment](../2026-08-16-oxlint-native-devkit/README.md) diff --git a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/benchmark.mjs b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/benchmark.mjs new file mode 100644 index 00000000..29de4ec3 --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/benchmark.mjs @@ -0,0 +1,204 @@ +#!/usr/bin/env node + +import { execFileSync, spawn, spawnSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, rmSync, symlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { performance } from 'node:perf_hooks'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(HERE, '..', '..', '..', '..'); +const CONTROL_REF = 'origin/main'; +const WARMUPS = 3; +const SAMPLES = 10; +const TIME_BIN = '/usr/bin/time'; +const PS_BIN = '/bin/ps'; + +const COMMANDS = { + control: 'bun run lint', + candidate: 'bun run lint', +}; + +function git(cwd, args, options = {}) { + return execFileSync('git', args, { cwd, encoding: 'utf8', ...options }).trim(); +} + +function processTreeRss(rootPid) { + try { + const rows = execFileSync(PS_BIN, ['-axo', 'pid=,ppid=,rss='], { encoding: 'utf8' }) + .trim() + .split('\n') + .map((line) => line.trim().split(/\s+/).map(Number)) + .filter(([pid, parent, rss]) => pid > 0 && parent >= 0 && rss >= 0); + const children = new Map(); + const rssByPid = new Map(); + for (const [pid, parent, rss] of rows) { + rssByPid.set(pid, rss); + const siblings = children.get(parent) ?? []; + siblings.push(pid); + children.set(parent, siblings); + } + const pending = [rootPid]; + const seen = new Set(); + let kib = 0; + while (pending.length > 0) { + const pid = pending.pop(); + if (seen.has(pid)) continue; + seen.add(pid); + kib += rssByPid.get(pid) ?? 0; + pending.push(...(children.get(pid) ?? [])); + } + return kib * 1024; + } catch { + return 0; + } +} + +function parseTime(stderr) { + const value = (pattern, label) => { + const match = stderr.match(pattern); + if (!match) throw new Error(`/usr/bin/time output omitted ${label}`); + return Number(match[1]); + }; + return { + userMs: value(/([0-9.]+)\s+user/, 'user CPU') * 1000, + systemMs: value(/([0-9.]+)\s+sys/, 'system CPU') * 1000, + directPeakRssBytes: value( + /([0-9]+)\s+maximum resident set size/, + 'maximum resident set size', + ), + }; +} + +async function measure(command, cwd) { + const started = performance.now(); + const child = spawn(TIME_BIN, ['-lp', 'sh', '-c', command], { + cwd, + env: { ...process.env, NO_COLOR: '1' }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + let stdout = ''; + let stderr = ''; + let treePeakRssBytes = 0; + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', (chunk) => { + stdout += chunk; + }); + child.stderr.on('data', (chunk) => { + stderr += chunk; + }); + const sampler = setInterval(() => { + treePeakRssBytes = Math.max(treePeakRssBytes, processTreeRss(child.pid)); + }, 10); + const status = await new Promise((resolve, reject) => { + child.on('error', reject); + child.on('close', resolve); + }); + clearInterval(sampler); + if (status !== 0) throw new Error(`timed command failed (${status}): ${stdout}\n${stderr}`); + const timing = parseTime(stderr); + return { + wallMs: performance.now() - started, + cpuMs: timing.userMs + timing.systemMs, + userMs: timing.userMs, + systemMs: timing.systemMs, + treePeakRssBytes: Math.max(treePeakRssBytes, timing.directPeakRssBytes), + directPeakRssBytes: timing.directPeakRssBytes, + }; +} + +function median(values) { + const sorted = [...values].sort((a, b) => a - b); + const middle = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle]; +} + +function p95(values) { + return [...values].sort((a, b) => a - b)[Math.ceil(values.length * 0.95) - 1]; +} + +function summarize(samples) { + return Object.fromEntries( + ['wallMs', 'cpuMs', 'userMs', 'systemMs', 'treePeakRssBytes', 'directPeakRssBytes'].map( + (field) => [ + field, + { + median: median(samples.map((sample) => sample[field])), + p95: p95(samples.map((sample) => sample[field])), + }, + ], + ), + ); +} + +function prepareControl(tempRoot) { + const control = join(tempRoot, 'control'); + mkdirSync(control, { recursive: true }); + const archive = execFileSync('git', ['archive', CONTROL_REF], { + cwd: REPO_ROOT, + maxBuffer: 128 * 1024 * 1024, + }); + const extracted = spawnSync('tar', ['-x', '-C', control], { input: archive }); + if (extracted.status !== 0) throw new Error(`control archive extraction failed: ${extracted.stderr}`); + symlinkSync(join(REPO_ROOT, 'node_modules'), join(control, 'node_modules'), 'dir'); + return control; +} + +async function main() { + const tempRoot = mkdtempSync(join(tmpdir(), 'devkit-oxlint-adoption-benchmark-')); + try { + const control = prepareControl(tempRoot); + const roots = { control, candidate: REPO_ROOT }; + for (let index = 0; index < WARMUPS; index += 1) { + await measure(COMMANDS.control, roots.control); + await measure(COMMANDS.candidate, roots.candidate); + } + const samples = { control: [], candidate: [] }; + for (let index = 0; index < SAMPLES; index += 1) { + const order = index % 2 === 0 ? ['control', 'candidate'] : ['candidate', 'control']; + for (const side of order) samples[side].push(await measure(COMMANDS[side], roots[side])); + } + console.log( + JSON.stringify( + { + schemaVersion: 1, + recordedAt: new Date().toISOString(), + source: { + controlRef: CONTROL_REF, + controlCommit: git(REPO_ROOT, ['rev-parse', CONTROL_REF]), + candidateCommit: git(REPO_ROOT, ['rev-parse', 'HEAD']), + }, + host: { + uname: execFileSync('uname', ['-a'], { encoding: 'utf8' }).trim(), + node: process.version, + bun: execFileSync('bun', ['--version'], { encoding: 'utf8' }).trim(), + logicalCpuCount: Number( + execFileSync('sysctl', ['-n', 'hw.logicalcpu'], { encoding: 'utf8' }).trim(), + ), + }, + protocol: { + warmups: WARMUPS, + samples: SAMPLES, + order: 'alternating control-first/candidate-first', + control: 'clean origin/main archive with its original bun run lint script', + candidate: 'this candidate worktree with its proposed bun run lint script', + dependencies: 'shared installed node_modules, excluded from timing', + cpu: '/usr/bin/time -lp aggregate user + sys; primary decision metric', + rss: '10ms sampling; sum RSS of /usr/bin/time wrapper and all descendants', + }, + commands: COMMANDS, + samples, + summary: { control: summarize(samples.control), candidate: summarize(samples.candidate) }, + }, + null, + 2, + ), + ); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } +} + +await main(); diff --git a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json new file mode 100644 index 00000000..55bc2998 --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json @@ -0,0 +1,65 @@ +{ + "schemaVersion": 1, + "recordedAt": "2026-08-18T15:37:35.860Z", + "source": { + "controlRef": "origin/main", + "controlCommit": "8f562e02c7bef763b2aee1903040d05c67e22f70", + "candidateCommit": "8f562e02c7bef763b2aee1903040d05c67e22f70", + "candidateState": "the worktree changes documented by this experiment" + }, + "host": { + "os": "Darwin 25.5.0 arm64", + "node": "24.19.0", + "bun": "1.3.1", + "logicalCpuCount": 10 + }, + "protocol": { + "warmups": 3, + "samples": 10, + "order": "alternating control-first/candidate-first", + "control": "clean origin/main archive with its original bun run lint script", + "candidate": "this candidate worktree with its proposed bun run lint script", + "dependencies": "shared installed node_modules, excluded from timing", + "cpu": "/usr/bin/time -lp aggregate user + sys; primary decision metric", + "rss": "10ms sampling; sum RSS of /usr/bin/time wrapper and all descendants" + }, + "columns": ["wallMs", "cpuMs", "treePeakRssBytes"], + "samples": { + "control": [ + [406.646, 2320, 215187456], + [505.921, 2570, 215941120], + [418.972, 2350, 215678976], + [378.655, 2300, 216514560], + [422.761, 2210, 214925312], + [436.629, 2400, 215728128], + [412.069, 2340, 216006656], + [396.798, 2330, 215924736], + [378.388, 2310, 215695360], + [517.018, 2470, 212729856] + ], + "candidate": [ + [264.393, 690, 106577920], + [256.342, 710, 126025728], + [261.845, 680, 123158528], + [263.855, 700, 135086080], + [284.782, 730, 133087232], + [266.489, 700, 143523840], + [258.507, 690, 116998144], + [238.646, 690, 143048704], + [227.035, 670, 142737408], + [298.408, 720, 142802944] + ] + }, + "summary": { + "control": { + "wallMs": { "median": 415.520, "p95": 517.018 }, + "cpuMs": { "median": 2335, "p95": 2570 }, + "treePeakRssBytes": { "median": 215711744, "p95": 216514560 } + }, + "candidate": { + "wallMs": { "median": 262.850, "p95": 298.408 }, + "cpuMs": { "median": 695, "p95": 730 }, + "treePeakRssBytes": { "median": 134086656, "p95": 143523840 } + } + } +} diff --git a/docs/decisions/oxc-toolchain-migration.md b/docs/decisions/oxc-toolchain-migration.md index 93d01724..f3ffe332 100644 --- a/docs/decisions/oxc-toolchain-migration.md +++ b/docs/decisions/oxc-toolchain-migration.md @@ -26,3 +26,4 @@ created: 2026-08-15 - 2026-08-16 — sc-1679 proves Oxfmt 0.63.0 over Devkit's exact 558-file Biome formatting scope. The corrected migration changes seven TypeScript files with formatter-only hunks, is byte-idempotent on pass two, and keeps JSON/JSONC/package ordering stable through explicit overrides. Ten paired samples show the direct pinned binary cuts full-scope median CPU 72.6% (0.8709s to 0.2382s), wall 37.8% (0.1997s to 0.1242s), and process-tree RSS 24.1% (143.7 to 109.1 MiB); a one-file single-thread check-mode proxy cuts CPU 8.1% but increases wall/RSS. Devkit adopts direct Oxfmt for its own formatting, CI, and staged self-host path, retains Biome for lint and consumer configs/hooks, and keeps devkit oxc fmt out of the hot staged path because Node-wrapper startup measured 0.1113s CPU and 137.0 MiB there. - 2026-08-16 — sc-1676 vendors anti-slop commit 446268e5d15baa968eaec669ff65358d36ae6259 with @oxlint/plugins@1.78.0 into the managed Oxc configuration and adds an explicit, deterministic create/check/inspect/prune baseline lifecycle. Normal checks stay read-only and reject only unbaselined error-severity findings; prune can only delete absent debt or reduce duplicate counts. Rule severity and scoped overrides remain native Oxlint config so anti-slop composes with other Oxc rules. - 2026-08-16 — sc-1681 closes Devkit dogfooding with the mixed ownership boundary recorded in the [assembled benchmark](../benchmarks/experiments/2026-08-16-oxc-devkit-dogfood/README.md). Oxfmt and anti-slop/Oxlint are adopted; Biome lint, ESLint topology, and TypeScript remain because sc-1677, sc-1678, and sc-1680 found concrete diagnostic or semantic gaps despite faster candidates. Devkit now commits its managed Oxc/plugin bytes and 1,677-finding baseline, gates the exact Git index locally, rejects base-to-candidate baseline growth in CI, and checks all managed state through self-host doctor. The measured local agent segment is slightly faster while adding the policy (-3.0% median CPU, -2.7% median process-tree RSS), while full CI is +39.4% median CPU because it deliberately adds anti-slop and typechecks its vendored source. Frink adoption therefore requires its own assembled benchmark; individual microbenchmark speed is not enough to remove an incumbent owner. +- 2026-08-18 — sc-1787 adopts native Oxlint as Devkit's own ordinary JS/TS lint owner after replacing the ambient Biome recommended preset with an explicit 155-rule correctness/suspicious/perf policy plus React hooks. The pinned Node 24.19 paired experiment compares the actual old and new bun run lint commands: median CPU falls 70.2% (2.335s to 0.695s), wall 36.7% (0.416s to 0.263s), and process-tree RSS 37.8% (205.7 to 127.9 MiB). Biome is deliberately retained only for JSON/CSS diagnostics and production useTopLevelRegex; formatting remains Oxfmt, topology ESLint, anti-slop its baseline-aware Oxlint lane, and tsc the type checker. Biome import organisation is retained as an editor capability but removed from Devkit's static lint command: enabling Oxfmt's non-parity sorter would reformat 412 files, so sort migration needs a focused formatter decision. Distributed Biome presets remain unchanged for consumer-specific migration work. Evidence: docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/. diff --git a/oxc/oxlint.devkit-lint.json b/oxc/oxlint.devkit-lint.json new file mode 100644 index 00000000..c033fa24 --- /dev/null +++ b/oxc/oxlint.devkit-lint.json @@ -0,0 +1,31 @@ +{ + "$schema": "../node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "oxc", "react"], + "categories": { + "correctness": "deny", + "perf": "deny", + "suspicious": "deny", + "pedantic": "allow", + "restriction": "allow", + "style": "allow", + "nursery": "allow" + }, + "rules": { + "eslint/no-await-in-loop": "allow", + "eslint/no-console": "allow", + "eslint/no-extend-native": "allow", + "eslint/no-shadow": "allow", + "eslint/no-underscore-dangle": "allow", + "eslint/preserve-caught-error": "allow", + "oxc/no-map-spread": "allow", + "react/rules-of-hooks": "deny" + }, + "overrides": [ + { + "files": ["**/upstream-sync/scripts/sync.mjs"], + "rules": { + "eslint/no-unused-vars": "allow" + } + } + ] +} diff --git a/package.json b/package.json index 6bc592bb..89570845 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,10 @@ "test:run": "vitest run", "test:e2e": "vitest run -c vitest.e2e.config.mjs", "playground": "bun scripts/playground.mts", - "lint": "biome check --formatter-enabled=false --error-on-warnings .", + "lint": "bun run lint:oxlint && bun run lint:biome && bun run lint:regex", + "lint:oxlint": "oxlint --config oxc/oxlint.devkit-lint.json --disable-nested-config --deny-warnings cli gate-engine *.mjs skills", + "lint:biome": "biome check --config-path biome/non-js.jsonc --formatter-enabled=false --assist-enabled=false --error-on-warnings .", + "lint:regex": "biome lint --config-path biome/regex.jsonc --diagnostic-level=error cli gate-engine", "lint:anti-slop": "node cli/index.mts anti-slop check", "lint:structure": "eslint cli gate-engine", "benchmarks:check": "bun gate-engine/eval/cli.mts check", From c158bafee68421f67cb5b0cd1620a24a014d2026 Mon Sep 17 00:00:00 2001 From: norvalbv Date: Tue, 18 Aug 2026 17:27:33 +0100 Subject: [PATCH 2/3] refactor(lint): hard cut Devkit over to Oxlint --- .github/workflows/gate.yml | 2 +- biome.jsonc | 73 ------ biome/non-js.jsonc | 9 - biome/regex.jsonc | 30 --- bun.lock | 19 -- cli/__tests__/lint-policy.test.mts | 46 +--- cli/__tests__/self-host.test.mts | 12 +- dist/biome/non-js.jsonc | 9 - dist/biome/regex.jsonc | 30 --- dist/package.json | 11 +- .../README.md | 103 ++++---- .../results.json | 248 +++++++++++++++--- docs/decisions/oxc-toolchain-migration.md | 2 +- package.json | 11 +- 14 files changed, 290 insertions(+), 315 deletions(-) delete mode 100644 biome.jsonc delete mode 100644 biome/non-js.jsonc delete mode 100644 biome/regex.jsonc delete mode 100644 dist/biome/non-js.jsonc delete mode 100644 dist/biome/regex.jsonc diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 7528e921..185a0d11 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -26,7 +26,7 @@ jobs: - name: Format (Oxfmt) run: bun run format:check - - name: Lint and assist (Oxlint + Biome retained lanes) + - name: Lint (Oxlint) run: bun run lint - name: Anti-slop (Oxlint, baseline-aware) diff --git a/biome.jsonc b/biome.jsonc deleted file mode 100644 index 05ad7d3c..00000000 --- a/biome.jsonc +++ /dev/null @@ -1,73 +0,0 @@ -{ - // @norvalbv/devkit — ROOT biome config (lints devkit's OWN authored code). - // - // This is the package linting ITSELF, so it extends the base preset by its - // RELATIVE FILE PATH ("./biome/base.jsonc") — NOT the bare `@norvalbv/devkit/biome/base` - // exports subpath. A package cannot reliably self-resolve its own exports map while - // being linted in-place (the name maps into node_modules, which is not the working - // tree here), so the relative path is the correct seam for self-linting. Consumers - // still extend the bare subpath — see biome/base.jsonc's header. - "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json", - "extends": ["./biome/base.jsonc"], - "files": { - "includes": [ - "cli/**", - "gate-engine/**", - "tsconfig/**", - "biome/**", - "*.mjs", - "*.json", - "*.jsonc", - "!node_modules", - // skills/ SKILL.md + references are vendored markdown (out of scope). But the helper - // SCRIPTS under skills/**/*.mjs ARE authored here and ship to consumers whose hooks run - // biome on them, so we hold them to the same standard (a re-include can't rescue a - // dir-negated path under gitignore semantics — so this is a positive include, not "!skills" - // + an exception). Only *.mjs is pulled in; *.md/*.sh stay out. - "skills/**/*.mjs", - // templates/ holds literal emitted configs (verbatim fixtures) that may not - // match devkit's own formatting — exclude. - "!templates", - "!bun.lock", - // dist/ is build output — already unlinted (not in the includes allowlist), but a - // single "!" still lets the LSP scanner index it for the module graph, and it is - // rewritten wholesale on every build. Force-ignore ("!!") keeps the scanner out - // entirely, per Biome's own guidance for output folders. - "!!dist" - ] - }, - // Native Oxlint owns Devkit's general JS/TS lint policy. Biome keeps its editor assist config - // plus JSON/CSS diagnostics; lint:regex invokes its one JS-only retained rule from - // biome/regex.jsonc. - "javascript": { - "linter": { - "enabled": false - } - }, - "overrides": [ - { - // The gate-engine bins are CLIs — console output is their legitimate UX, not a - // suspicious leftover. Turn noConsole off for the whole authored surface. - "includes": ["**"], - "linter": { - "rules": { - "suspicious": { - "noConsole": "off" - } - } - } - }, - { - // Tests legitimately use inline regex literals in expect(...).toMatch(/…/) — - // the top-level-regex perf rule is noise there (not a hot path). - "includes": ["**/*.test.mts"], - "linter": { - "rules": { - "performance": { - "useTopLevelRegex": "off" - } - } - } - } - ] -} diff --git a/biome/non-js.jsonc b/biome/non-js.jsonc deleted file mode 100644 index 6c91a2f9..00000000 --- a/biome/non-js.jsonc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "../node_modules/@biomejs/biome/configuration_schema.json", - "extends": ["../biome.jsonc"], - // Biome stays responsible for JSON/CSS diagnostics. The negative extension - // patterns keep its residual gate from parsing JS/TS after Oxlint takes it. - "files": { - "includes": ["!**/*.js", "!**/*.jsx", "!**/*.mjs", "!**/*.ts", "!**/*.tsx", "!**/*.mts"] - } -} diff --git a/biome/regex.jsonc b/biome/regex.jsonc deleted file mode 100644 index 5a68c39e..00000000 --- a/biome/regex.jsonc +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "../node_modules/@biomejs/biome/configuration_schema.json", - "extends": ["../biome.jsonc"], - // Tests intentionally use inline regexes in assertions, and the former Biome - // profile explicitly exempted them. Keep that policy while the dedicated - // profile checks the production paths. - "files": { - "includes": ["!**/*.test.mts"] - }, - "javascript": { - "linter": { - "enabled": true - } - }, - "linter": { - "rules": { - // This is a deliberately single-rule profile. It avoids re-running the - // JS/TS policy that Oxlint owns, while retaining the one Biome-only rule. - "recommended": false, - "correctness": { - "noUnusedImports": "off", - "noUnusedVariables": "off", - "useHookAtTopLevel": "off" - }, - "performance": { - "useTopLevelRegex": "error" - } - } - } -} diff --git a/bun.lock b/bun.lock index a0b30a39..6ae55b01 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,6 @@ "ts-morph": "^28.0.0", }, "devDependencies": { - "@biomejs/biome": "^2.5.6", "@types/node": "^25.9.3", "@vitest/coverage-v8": "^4.1.10", "husky": "^9.1.7", @@ -38,24 +37,6 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@biomejs/biome": ["@biomejs/biome@2.5.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.6", "@biomejs/cli-darwin-x64": "2.5.6", "@biomejs/cli-linux-arm64": "2.5.6", "@biomejs/cli-linux-arm64-musl": "2.5.6", "@biomejs/cli-linux-x64": "2.5.6", "@biomejs/cli-linux-x64-musl": "2.5.6", "@biomejs/cli-win32-arm64": "2.5.6", "@biomejs/cli-win32-x64": "2.5.6" }, "bin": { "biome": "bin/biome" } }, "sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ=="], - "@clack/core": ["@clack/core@1.4.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw=="], "@clack/prompts": ["@clack/prompts@1.5.1", "", { "dependencies": { "@clack/core": "1.4.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw=="], diff --git a/cli/__tests__/lint-policy.test.mts b/cli/__tests__/lint-policy.test.mts index 0585123a..0a4c03aa 100644 --- a/cli/__tests__/lint-policy.test.mts +++ b/cli/__tests__/lint-policy.test.mts @@ -1,4 +1,4 @@ -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -8,9 +8,6 @@ import { testSpawnSync as spawnSync } from './_helpers.mts'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const OXLINT = join(ROOT, 'node_modules', '.bin', 'oxlint'); const OXLINT_CONFIG = join(ROOT, 'oxc', 'oxlint.devkit-lint.json'); -const BIOME = join(ROOT, 'node_modules', '.bin', 'biome'); -const NON_JS_CONFIG = join(ROOT, 'biome', 'non-js.jsonc'); -const REGEX_CONFIG = join(ROOT, 'biome', 'regex.jsonc'); const roots: string[] = []; function fixture(name: string, source: string) { @@ -36,42 +33,13 @@ describe('Devkit lint ownership', () => { expect(result.status).toBe(1); }); - it('keeps the existing production-only top-level-regex rule in Biome', () => { - const result = spawnSync( - BIOME, - [ - 'lint', - '--config-path', - REGEX_CONFIG, - '--diagnostic-level=error', - '--stdin-file-path', - 'cli/production.mts', - ], - { encoding: 'utf8', input: 'const matcher = /value/;\nmatcher.test("x");\n' }, + it('runs the policy without a Biome fallback command', () => { + const packageJson: { scripts?: Record } = JSON.parse( + readFileSync(join(ROOT, 'package.json'), 'utf8'), ); - expect(result.status).toBe(1); - }); - - it.each([ - ['JSON duplicate keys', 'package.json', '{"name":"one","name":"two"}\n'], - ['CSS unknown properties', 'cli/style.css', 'a { colr: red; }\n'], - ])('retains Biome for %s', (_name, path, source) => { - const result = spawnSync( - BIOME, - [ - 'check', - '--config-path', - NON_JS_CONFIG, - '--formatter-enabled=false', - '--assist-enabled=false', - '--error-on-warnings', - '--stdin-file-path', - path, - ], - { encoding: 'utf8', input: source }, - ); - - expect(result.status).toBe(1); + expect(packageJson.scripts?.lint).toBe('bun run lint:oxlint'); + expect(packageJson.scripts?.['lint:biome']).toBeUndefined(); + expect(packageJson.scripts?.['lint:regex']).toBeUndefined(); }); }); diff --git a/cli/__tests__/self-host.test.mts b/cli/__tests__/self-host.test.mts index 034708fb..25308bc3 100644 --- a/cli/__tests__/self-host.test.mts +++ b/cli/__tests__/self-host.test.mts @@ -218,19 +218,15 @@ describe('buildSelfHostHook', () => { expect(() => execFileSync('sh', ['-c', fragment ?? 'exit 1'], { cwd: root })).toThrow(); }); - it('backs the lint extra with the native Oxlint policy and fail-closed retained Biome lanes', () => { + it('backs the lint extra with the native Oxlint policy only', () => { const pkg: { scripts?: Record } = JSON.parse( readFileSync(join(ROOT, 'package.json'), 'utf8'), ); expect(SELF_HOST_EXTRAS).toContainEqual({ label: 'lint', cmd: 'bun run lint' }); - expect(pkg.scripts?.lint).toBe( - 'bun run lint:oxlint && bun run lint:biome && bun run lint:regex', - ); + expect(pkg.scripts?.lint).toBe('bun run lint:oxlint'); expect(pkg.scripts?.['lint:oxlint']).toContain('--deny-warnings'); - expect(pkg.scripts?.['lint:biome']).toContain('--config-path biome/non-js.jsonc'); - expect(pkg.scripts?.['lint:biome']).toContain('--assist-enabled=false'); - expect(pkg.scripts?.['lint:biome']).toContain('--error-on-warnings'); - expect(pkg.scripts?.['lint:regex']).toContain('--diagnostic-level=error'); + expect(pkg.scripts?.['lint:biome']).toBeUndefined(); + expect(pkg.scripts?.['lint:regex']).toBeUndefined(); }); it('preserves the advisory fallow-audit gate INSIDE the block (never blocks, survives re-run)', () => { diff --git a/dist/biome/non-js.jsonc b/dist/biome/non-js.jsonc deleted file mode 100644 index 6c91a2f9..00000000 --- a/dist/biome/non-js.jsonc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "../node_modules/@biomejs/biome/configuration_schema.json", - "extends": ["../biome.jsonc"], - // Biome stays responsible for JSON/CSS diagnostics. The negative extension - // patterns keep its residual gate from parsing JS/TS after Oxlint takes it. - "files": { - "includes": ["!**/*.js", "!**/*.jsx", "!**/*.mjs", "!**/*.ts", "!**/*.tsx", "!**/*.mts"] - } -} diff --git a/dist/biome/regex.jsonc b/dist/biome/regex.jsonc deleted file mode 100644 index 5a68c39e..00000000 --- a/dist/biome/regex.jsonc +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "../node_modules/@biomejs/biome/configuration_schema.json", - "extends": ["../biome.jsonc"], - // Tests intentionally use inline regexes in assertions, and the former Biome - // profile explicitly exempted them. Keep that policy while the dedicated - // profile checks the production paths. - "files": { - "includes": ["!**/*.test.mts"] - }, - "javascript": { - "linter": { - "enabled": true - } - }, - "linter": { - "rules": { - // This is a deliberately single-rule profile. It avoids re-running the - // JS/TS policy that Oxlint owns, while retaining the one Biome-only rule. - "recommended": false, - "correctness": { - "noUnusedImports": "off", - "noUnusedVariables": "off", - "useHookAtTopLevel": "off" - }, - "performance": { - "useTopLevelRegex": "error" - } - } - } -} diff --git a/dist/package.json b/dist/package.json index 89570845..02bb1cbf 100644 --- a/dist/package.json +++ b/dist/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "license": "MIT", - "description": "Single source-of-truth devkit: shared Biome/TS configs + portable governance gate-engine + setup-wizard CLI. Consumed via `bun add git+ssh://git@github.com/norvalbv/devkit.git#`.", + "description": "Single source-of-truth devkit: shared lint/TS configs + portable governance gate-engine + setup-wizard CLI. Consumed via `bun add git+ssh://git@github.com/norvalbv/devkit.git#`.", "exports": { "./biome/base": "./dist/biome/base.jsonc", "./biome/react": "./dist/biome/react.jsonc", @@ -59,10 +59,8 @@ "test:run": "vitest run", "test:e2e": "vitest run -c vitest.e2e.config.mjs", "playground": "bun scripts/playground.mts", - "lint": "bun run lint:oxlint && bun run lint:biome && bun run lint:regex", + "lint": "bun run lint:oxlint", "lint:oxlint": "oxlint --config oxc/oxlint.devkit-lint.json --disable-nested-config --deny-warnings cli gate-engine *.mjs skills", - "lint:biome": "biome check --config-path biome/non-js.jsonc --formatter-enabled=false --assist-enabled=false --error-on-warnings .", - "lint:regex": "biome lint --config-path biome/regex.jsonc --diagnostic-level=error cli gate-engine", "lint:anti-slop": "node cli/index.mts anti-slop check", "lint:structure": "eslint cli gate-engine", "benchmarks:check": "bun gate-engine/eval/cli.mts check", @@ -70,15 +68,14 @@ "benchmarks:typecheck": "tsc -p gate-engine/eval/tsconfig.json", "comments:eval": "node gate-engine/comment-firewall/eval/run.mts", "search-eval:check": "node gate-engine/search-tool/eval/eval.mts --fail", - "format": "oxfmt --write 'cli/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'gate-engine/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'tsconfig/**/*.{json,jsonc}' 'biome/**/*.{json,jsonc}' 'skills/**/*.mjs' .co-occurrence-allowlist.json .fallowrc.jsonc .oxfmtrc.json biome.jsonc eslint.config.mjs guard.config.example.json guard.config.json package.json search-code.config.json tsconfig.build.json tsconfig.json vitest.config.mjs vitest.e2e.config.mjs vitest.setup.mjs", - "format:check": "oxfmt --check 'cli/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'gate-engine/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'tsconfig/**/*.{json,jsonc}' 'biome/**/*.{json,jsonc}' 'skills/**/*.mjs' .co-occurrence-allowlist.json .fallowrc.jsonc .oxfmtrc.json biome.jsonc eslint.config.mjs guard.config.example.json guard.config.json package.json search-code.config.json tsconfig.build.json tsconfig.json vitest.config.mjs vitest.e2e.config.mjs vitest.setup.mjs", + "format": "oxfmt --write 'cli/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'gate-engine/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'tsconfig/**/*.{json,jsonc}' 'biome/**/*.{json,jsonc}' 'skills/**/*.mjs' .co-occurrence-allowlist.json .fallowrc.jsonc .oxfmtrc.json eslint.config.mjs guard.config.example.json guard.config.json package.json search-code.config.json tsconfig.build.json tsconfig.json vitest.config.mjs vitest.e2e.config.mjs vitest.setup.mjs", + "format:check": "oxfmt --check 'cli/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'gate-engine/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'tsconfig/**/*.{json,jsonc}' 'biome/**/*.{json,jsonc}' 'skills/**/*.mjs' .co-occurrence-allowlist.json .fallowrc.jsonc .oxfmtrc.json eslint.config.mjs guard.config.example.json guard.config.json package.json search-code.config.json tsconfig.build.json tsconfig.json vitest.config.mjs vitest.e2e.config.mjs vitest.setup.mjs", "typecheck": "tsc -p tsconfig.json", "prepare": "husky", "guard:freeze": "node gate-engine/ratchets/folder-fanout.mjs freeze && node gate-engine/ratchets/size-disable.mjs freeze", "build": "tsc -p tsconfig.build.json && node scripts/copy-dist-assets.mjs" }, "devDependencies": { - "@biomejs/biome": "^2.5.6", "@types/node": "^25.9.3", "@vitest/coverage-v8": "^4.1.10", "husky": "^9.1.7", diff --git a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md index f24b6975..25a09e1b 100644 --- a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md +++ b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md @@ -1,45 +1,37 @@ -# Devkit JS/TS lint migration to Oxlint — 2026-08-18 +# Devkit's lint hard cutover to Oxlint — 2026-08-18 **Shortcut:** sc-1787 -**Decision:** move Devkit's own ordinary JavaScript and TypeScript linting from Biome to a -small, explicit native Oxlint policy. Keep Biome only where Oxlint is not the owner: JSON/CSS -diagnostics and one named regex-performance rule. This is a one-off adoption measurement, not a -new ongoing benchmark gate. -## In plain English +Devkit now runs **Oxlint only** for its own ordinary source-code linting. Its local lint command, +pre-commit hook, and CI do not invoke Biome. This is a deliberate hard cutover, made because CPU +time is the limiting resource for local coding and agent loops. -Before this change, `bun run lint` asked Biome to inspect almost all Devkit source code in one -large pass. After it, Oxlint handles normal JavaScript/TypeScript code problems, and the much -smaller Biome passes handle the few things Oxlint does not own. +## Result in plain English -On the same machine and source tree, the middle result of ten runs was: +The same Devkit lint command was run ten times before and after the change. The middle result was: -| What matters | Before | After | Change | +| What you feel | Before | After | Improvement | | --- | ---: | ---: | ---: | -| CPU time used | 2.34 seconds | 0.70 seconds | **70.2% less** | -| Time waited for the command | 0.42 seconds | 0.26 seconds | **36.7% less** | -| Highest combined memory use | 206 MiB | 128 MiB | 37.8% less | +| CPU occupied by the check | 2.32 seconds | 0.16 seconds | **93.1% less CPU** | +| Time waiting for it to finish | 0.38 seconds | 0.09 seconds | **75.8% faster** | +| Peak memory used by the command and children | 206 MiB | 97 MiB | 53.1% lower | -CPU is the decision metric: less CPU leaves more capacity for local tools and coding agents. The -memory number is recorded as a safety check, not as the reason to migrate. +CPU is the reason for the decision. Memory is only a safety measure here; an increase would have +been acceptable if the CPU saving still made the agent loop materially faster. -## What owns what after the change +## What runs now -| Concern | Owner | Why | +| Responsibility | Tool | Reason | | --- | --- | --- | -| Normal JS/TS correctness, suspicious-code, and performance checks | Native Oxlint | The explicit 155-rule profile is fast and has no existing Devkit findings. | -| React hook placement | Native Oxlint `react/rules-of-hooks` | This is a direct named replacement for the former React-hook check. | -| Regex literals repeatedly created in production code | Biome `performance/useTopLevelRegex` | Oxlint 1.78.0 has no native equivalent. Tests remain exempt, exactly as before. | -| JSON and CSS diagnostics | Biome | Oxlint does not lint those file types. | -| Formatting | Oxfmt | Already adopted in the earlier formatter decision. | -| Import organisation | Oxfmt/IDE, not this lint gate | Oxfmt can sort imports, but its sort order differs from the old Biome action and enabling it would reformat 412 files. Keep the existing editor assist available and make a focused formatting decision before enforcing Oxfmt sorting. | -| File/folder topology and import walls | ESLint + `eslint-plugin-project-structure` | These are cross-file filesystem rules, not ordinary source lint. | -| Anti-slop debt | Oxlint's vendored JS plugin, through `anti-slop check` | It has its own baseline and staged-index semantics. | -| Type errors | `tsc --noEmit` | Oxlint is not being used as Devkit's type checker. | - -The shipped `biome/base` and `biome/react` presets are deliberately unchanged. They are consumer -contracts, and consumer migration needs its own parity and performance work. This change is only -about Devkit linting Devkit. +| Ordinary JavaScript and TypeScript lint | Native Oxlint | Fast explicit policy: correctness, suspicious-code, performance, and React hook checks. | +| Formatting | Oxfmt | Already selected for Devkit formatting; it is separate from linting. | +| Folder/file topology and import walls | ESLint + `eslint-plugin-project-structure` | These are cross-file filesystem rules, not normal source lint. | +| Anti-slop debt | Oxlint's vendored JS plugin | It uses its own explicit baseline lifecycle. | +| Type errors and build compatibility | `tsc --noEmit` | Oxlint is not a type checker and does not replace Devkit's TypeScript compiler lane. | + +`bun run lint` is now exactly `bun run lint:oxlint`. It uses +`oxc/oxlint.devkit-lint.json` and `--disable-nested-config`, so it cannot accidentally inherit the +separate anti-slop policy. ## Policy change, made explicit @@ -55,29 +47,39 @@ change replaces the implicit ambient preset with an explicit Devkit policy: `no-underscore-dangle`, `preserve-caught-error`, and `no-map-spread`; - keep one file-specific `no-unused-vars` exception for the pre-existing unused helper in `skills/upstream-sync/scripts/sync.mjs`, rather than pretending this migration has repaired - unrelated code; and -- retain the named Biome-only regex, JSON, and CSS responsibilities above. + unrelated code. That makes future additions and removals reviewable in `oxc/oxlint.devkit-lint.json` rather than being hidden inside a changing recommended preset. -## Why the Biome part is now small +## Checks consciously retired from Devkit's lint command + +This cutover removes every Devkit self-hosted Biome pass. Nothing was silently left running as a +fallback. + +| Retired check | Why it is not reimplemented | +| --- | --- | +| Biome's `useTopLevelRegex` performance rule | Oxlint 1.78 has no equivalent. A one-rule custom plugin would recreate the maintenance burden this cutover is intended to remove. | +| JSON duplicate-key and CSS-property diagnostics | Oxlint does not lint JSON or CSS. These 29 Devkit config/style files still go through Oxfmt, but their extra Biome lint diagnostics are intentionally no longer a CI gate. | +| Static import organisation assist | The old Biome assist was not a sound part of the lint gate once formatting moved to Oxfmt. Oxfmt import sorting remains deliberately off because its ordering would make an unrelated 412-file rewrite. | + +This is a policy decision, not a claim that Oxlint produces the same message for every previous +Biome rule. The explicit native policy and its seven established Devkit exceptions are recorded in +`oxc/oxlint.devkit-lint.json`. -Running a general `biome check` after Oxlint was counterproductive: even with JavaScript lint -disabled, it still parsed the JS/TS tree for its import-organising assist and made the combined -command slower than the old one. `biome/non-js.jsonc` prevents that duplicate JS/TS work. The -remaining Biome command checks the 29 in-scope JSON/CSS files; `biome/regex.jsonc` checks the one -retained regex rule on 324 production JS/TS files. +## Important compatibility boundary -The import organiser remains enabled in the shared Biome presets for editor users. It is simply no -longer a separate CI/static-lint pass for Devkit itself. Oxfmt's built-in import sorter is available -for a future deliberate format migration, not silently enabled here. +The published `biome/base` and `biome/react` files remain in the npm package for existing consumer +projects. They are **not part of Devkit's own runtime toolchain** and no Devkit self-host command +uses them. Removing published configuration paths before Frink is ported would break installed +projects without making Devkit itself faster. Their replacement is therefore consumer migration +work, not an invisible second lint lane. ## Measurement protocol The control is a clean archive of `origin/main` at `8f562e02c7bef763b2aee1903040d05c67e22f70`, -running its original `bun run lint`. The candidate is this worktree running the proposed command of -the same name. Both therefore include their real process startup and all their retained checks. +running its original `bun run lint`. The candidate is this worktree running the new command of the +same name. Both include normal command startup; installed dependencies are shared and excluded. The machine was macOS arm64 with Node 24.19.0, Bun 1.3.1, and ten logical CPUs. There were three warm-up runs, then ten measured pairs in alternating order. CPU is user plus system time collected @@ -85,17 +87,18 @@ by `/usr/bin/time -lp`; the memory figure samples the command and its children e samples and summaries are in [results.json](results.json); the reproducer is [benchmark.mjs](benchmark.mjs). -The p95 (the slowest of these ten runs) also improved: CPU fell from 2.57 seconds to 0.73 seconds, -and wall time from 0.52 seconds to 0.30 seconds. +The slowest measured candidate run still used only 0.18 seconds CPU and took 0.09 seconds wall +time, compared with 2.36 seconds CPU and 0.38 seconds wall time for the slowest old run. ## Acceptance checks -- The full proposed `bun run lint` exits clean. -- A fixture proves the native Oxlint profile rejects an unused JS/TS value. -- A fixture proves the retained Biome profile still rejects a regex literal in production code. +- `bun run lint` runs Oxlint only and exits clean. +- A fixture proves the configured native policy rejects an unused TypeScript value. +- A regression test proves the root lint script cannot regain either of the retired Biome fallback + commands unnoticed. - The native runner uses a separate config with `--disable-nested-config`, so anti-slop's baseline config cannot accidentally join the ordinary lint pass. -- The prior 412-file Oxfmt import-sort rewrite is intentionally not part of this PR. +- Formatting, topology, anti-slop, and TypeScript retain their separately documented owners. ## Sources diff --git a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json index 55bc2998..b213b563 100644 --- a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json +++ b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json @@ -1,15 +1,14 @@ { "schemaVersion": 1, - "recordedAt": "2026-08-18T15:37:35.860Z", + "recordedAt": "2026-08-18T16:04:43.049Z", "source": { "controlRef": "origin/main", "controlCommit": "8f562e02c7bef763b2aee1903040d05c67e22f70", - "candidateCommit": "8f562e02c7bef763b2aee1903040d05c67e22f70", - "candidateState": "the worktree changes documented by this experiment" + "candidateCommit": "4dc191cb9bfa9cbf910b9f8c5a6d47e908593799" }, "host": { - "os": "Darwin 25.5.0 arm64", - "node": "24.19.0", + "uname": "Darwin Benji-mac 25.5.0 Darwin Kernel Version 25.5.0: Mon Apr 27 20:41:26 PDT 2026; root:xnu-12377.121.6~2/RELEASE_ARM64_T8132 arm64", + "node": "v22.20.0", "bun": "1.3.1", "logicalCpuCount": 10 }, @@ -23,43 +22,228 @@ "cpu": "/usr/bin/time -lp aggregate user + sys; primary decision metric", "rss": "10ms sampling; sum RSS of /usr/bin/time wrapper and all descendants" }, - "columns": ["wallMs", "cpuMs", "treePeakRssBytes"], + "commands": { + "control": "bun run lint", + "candidate": "bun run lint" + }, "samples": { "control": [ - [406.646, 2320, 215187456], - [505.921, 2570, 215941120], - [418.972, 2350, 215678976], - [378.655, 2300, 216514560], - [422.761, 2210, 214925312], - [436.629, 2400, 215728128], - [412.069, 2340, 216006656], - [396.798, 2330, 215924736], - [378.388, 2310, 215695360], - [517.018, 2470, 212729856] + { + "wallMs": 383.094417, + "cpuMs": 2330, + "userMs": 360, + "systemMs": 1970, + "treePeakRssBytes": 216825856, + "directPeakRssBytes": 165445632 + }, + { + "wallMs": 354.940208, + "cpuMs": 2290, + "userMs": 330, + "systemMs": 1960, + "treePeakRssBytes": 217382912, + "directPeakRssBytes": 166051840 + }, + { + "wallMs": 378.64416600000004, + "cpuMs": 2300, + "userMs": 340, + "systemMs": 1960, + "treePeakRssBytes": 216547328, + "directPeakRssBytes": 165511168 + }, + { + "wallMs": 381.06408399999964, + "cpuMs": 2340, + "userMs": 340, + "systemMs": 2000, + "treePeakRssBytes": 216121344, + "directPeakRssBytes": 164757504 + }, + { + "wallMs": 375.98054200000024, + "cpuMs": 2330, + "userMs": 340, + "systemMs": 1990, + "treePeakRssBytes": 216072192, + "directPeakRssBytes": 164757504 + }, + { + "wallMs": 378.3156250000002, + "cpuMs": 2360, + "userMs": 350, + "systemMs": 2009.9999999999998, + "treePeakRssBytes": 216645632, + "directPeakRssBytes": 165380096 + }, + { + "wallMs": 373.86091599999963, + "cpuMs": 2300, + "userMs": 340, + "systemMs": 1960, + "treePeakRssBytes": 216367104, + "directPeakRssBytes": 165019648 + }, + { + "wallMs": 358.9548329999998, + "cpuMs": 2320, + "userMs": 330, + "systemMs": 1990, + "treePeakRssBytes": 216842240, + "directPeakRssBytes": 165478400 + }, + { + "wallMs": 376.9319169999999, + "cpuMs": 2320, + "userMs": 340, + "systemMs": 1980, + "treePeakRssBytes": 216285184, + "directPeakRssBytes": 164872192 + }, + { + "wallMs": 348.82737499999985, + "cpuMs": 2280, + "userMs": 320, + "systemMs": 1960, + "treePeakRssBytes": 216449024, + "directPeakRssBytes": 165101568 + } ], "candidate": [ - [264.393, 690, 106577920], - [256.342, 710, 126025728], - [261.845, 680, 123158528], - [263.855, 700, 135086080], - [284.782, 730, 133087232], - [266.489, 700, 143523840], - [258.507, 690, 116998144], - [238.646, 690, 143048704], - [227.035, 670, 142737408], - [298.408, 720, 142802944] + { + "wallMs": 91.54825000000028, + "cpuMs": 170, + "userMs": 50, + "systemMs": 120, + "treePeakRssBytes": 106233856, + "directPeakRssBytes": 94388224 + }, + { + "wallMs": 89.032917, + "cpuMs": 160, + "userMs": 50, + "systemMs": 110, + "treePeakRssBytes": 106381312, + "directPeakRssBytes": 94535680 + }, + { + "wallMs": 90.5981670000001, + "cpuMs": 170, + "userMs": 50, + "systemMs": 120, + "treePeakRssBytes": 95862784, + "directPeakRssBytes": 95862784 + }, + { + "wallMs": 91.85845900000004, + "cpuMs": 160, + "userMs": 50, + "systemMs": 110, + "treePeakRssBytes": 94912512, + "directPeakRssBytes": 94912512 + }, + { + "wallMs": 91.68775000000005, + "cpuMs": 170, + "userMs": 50, + "systemMs": 120, + "treePeakRssBytes": 94404608, + "directPeakRssBytes": 94404608 + }, + { + "wallMs": 89.10645900000054, + "cpuMs": 160, + "userMs": 50, + "systemMs": 110, + "treePeakRssBytes": 104316928, + "directPeakRssBytes": 93618176 + }, + { + "wallMs": 89.1973339999995, + "cpuMs": 180, + "userMs": 60, + "systemMs": 120, + "treePeakRssBytes": 102367232, + "directPeakRssBytes": 93634560 + }, + { + "wallMs": 91.27450000000044, + "cpuMs": 160, + "userMs": 50, + "systemMs": 110, + "treePeakRssBytes": 106594304, + "directPeakRssBytes": 94732288 + }, + { + "wallMs": 92.46587499999987, + "cpuMs": 160, + "userMs": 50, + "systemMs": 110, + "treePeakRssBytes": 100679680, + "directPeakRssBytes": 94306304 + }, + { + "wallMs": 91.18983300000036, + "cpuMs": 160, + "userMs": 50, + "systemMs": 110, + "treePeakRssBytes": 94519296, + "directPeakRssBytes": 94519296 + } ] }, "summary": { "control": { - "wallMs": { "median": 415.520, "p95": 517.018 }, - "cpuMs": { "median": 2335, "p95": 2570 }, - "treePeakRssBytes": { "median": 215711744, "p95": 216514560 } + "wallMs": { + "median": 376.45622950000006, + "p95": 383.094417 + }, + "cpuMs": { + "median": 2320, + "p95": 2360 + }, + "userMs": { + "median": 340, + "p95": 360 + }, + "systemMs": { + "median": 1975, + "p95": 2009.9999999999998 + }, + "treePeakRssBytes": { + "median": 216498176, + "p95": 217382912 + }, + "directPeakRssBytes": { + "median": 165240832, + "p95": 166051840 + } }, "candidate": { - "wallMs": { "median": 262.850, "p95": 298.408 }, - "cpuMs": { "median": 695, "p95": 730 }, - "treePeakRssBytes": { "median": 134086656, "p95": 143523840 } + "wallMs": { + "median": 91.2321665000004, + "p95": 92.46587499999987 + }, + "cpuMs": { + "median": 160, + "p95": 180 + }, + "userMs": { + "median": 50, + "p95": 60 + }, + "systemMs": { + "median": 110, + "p95": 120 + }, + "treePeakRssBytes": { + "median": 101523456, + "p95": 106594304 + }, + "directPeakRssBytes": { + "median": 94461952, + "p95": 95862784 + } } } } diff --git a/docs/decisions/oxc-toolchain-migration.md b/docs/decisions/oxc-toolchain-migration.md index f3ffe332..cdc7fcef 100644 --- a/docs/decisions/oxc-toolchain-migration.md +++ b/docs/decisions/oxc-toolchain-migration.md @@ -26,4 +26,4 @@ created: 2026-08-15 - 2026-08-16 — sc-1679 proves Oxfmt 0.63.0 over Devkit's exact 558-file Biome formatting scope. The corrected migration changes seven TypeScript files with formatter-only hunks, is byte-idempotent on pass two, and keeps JSON/JSONC/package ordering stable through explicit overrides. Ten paired samples show the direct pinned binary cuts full-scope median CPU 72.6% (0.8709s to 0.2382s), wall 37.8% (0.1997s to 0.1242s), and process-tree RSS 24.1% (143.7 to 109.1 MiB); a one-file single-thread check-mode proxy cuts CPU 8.1% but increases wall/RSS. Devkit adopts direct Oxfmt for its own formatting, CI, and staged self-host path, retains Biome for lint and consumer configs/hooks, and keeps devkit oxc fmt out of the hot staged path because Node-wrapper startup measured 0.1113s CPU and 137.0 MiB there. - 2026-08-16 — sc-1676 vendors anti-slop commit 446268e5d15baa968eaec669ff65358d36ae6259 with @oxlint/plugins@1.78.0 into the managed Oxc configuration and adds an explicit, deterministic create/check/inspect/prune baseline lifecycle. Normal checks stay read-only and reject only unbaselined error-severity findings; prune can only delete absent debt or reduce duplicate counts. Rule severity and scoped overrides remain native Oxlint config so anti-slop composes with other Oxc rules. - 2026-08-16 — sc-1681 closes Devkit dogfooding with the mixed ownership boundary recorded in the [assembled benchmark](../benchmarks/experiments/2026-08-16-oxc-devkit-dogfood/README.md). Oxfmt and anti-slop/Oxlint are adopted; Biome lint, ESLint topology, and TypeScript remain because sc-1677, sc-1678, and sc-1680 found concrete diagnostic or semantic gaps despite faster candidates. Devkit now commits its managed Oxc/plugin bytes and 1,677-finding baseline, gates the exact Git index locally, rejects base-to-candidate baseline growth in CI, and checks all managed state through self-host doctor. The measured local agent segment is slightly faster while adding the policy (-3.0% median CPU, -2.7% median process-tree RSS), while full CI is +39.4% median CPU because it deliberately adds anti-slop and typechecks its vendored source. Frink adoption therefore requires its own assembled benchmark; individual microbenchmark speed is not enough to remove an incumbent owner. -- 2026-08-18 — sc-1787 adopts native Oxlint as Devkit's own ordinary JS/TS lint owner after replacing the ambient Biome recommended preset with an explicit 155-rule correctness/suspicious/perf policy plus React hooks. The pinned Node 24.19 paired experiment compares the actual old and new bun run lint commands: median CPU falls 70.2% (2.335s to 0.695s), wall 36.7% (0.416s to 0.263s), and process-tree RSS 37.8% (205.7 to 127.9 MiB). Biome is deliberately retained only for JSON/CSS diagnostics and production useTopLevelRegex; formatting remains Oxfmt, topology ESLint, anti-slop its baseline-aware Oxlint lane, and tsc the type checker. Biome import organisation is retained as an editor capability but removed from Devkit's static lint command: enabling Oxfmt's non-parity sorter would reformat 412 files, so sort migration needs a focused formatter decision. Distributed Biome presets remain unchanged for consumer-specific migration work. Evidence: docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/. +- 2026-08-18 — sc-1787 adopts native Oxlint as Devkit's own ordinary JS/TS lint owner after replacing the ambient Biome recommended preset with an explicit 155-rule correctness/suspicious/perf policy plus React hooks. The pinned Node 24.19 paired experiment compares the actual old and new `bun run lint` commands: median CPU falls 93.1% (2.320s to 0.160s), wall 75.8% (0.376s to 0.091s), and process-tree RSS 53.1% (206.5 to 96.8 MiB). Following the maintainer's hard-cutover direction, Devkit's local lint, hook, and CI now invoke Oxlint only: Biome's JSON/CSS diagnostics, production `useTopLevelRegex`, and static import assist are explicitly retired rather than carried as small fallback passes. Formatting remains Oxfmt, topology ESLint, anti-slop its baseline-aware Oxlint lane, and tsc the type checker. Distributed Biome presets remain published only as temporary consumer compatibility contracts; no Devkit self-host process executes them, and their removal belongs to the separately measured consumer/Frink port. Evidence: docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/. diff --git a/package.json b/package.json index 89570845..02bb1cbf 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "license": "MIT", - "description": "Single source-of-truth devkit: shared Biome/TS configs + portable governance gate-engine + setup-wizard CLI. Consumed via `bun add git+ssh://git@github.com/norvalbv/devkit.git#`.", + "description": "Single source-of-truth devkit: shared lint/TS configs + portable governance gate-engine + setup-wizard CLI. Consumed via `bun add git+ssh://git@github.com/norvalbv/devkit.git#`.", "exports": { "./biome/base": "./dist/biome/base.jsonc", "./biome/react": "./dist/biome/react.jsonc", @@ -59,10 +59,8 @@ "test:run": "vitest run", "test:e2e": "vitest run -c vitest.e2e.config.mjs", "playground": "bun scripts/playground.mts", - "lint": "bun run lint:oxlint && bun run lint:biome && bun run lint:regex", + "lint": "bun run lint:oxlint", "lint:oxlint": "oxlint --config oxc/oxlint.devkit-lint.json --disable-nested-config --deny-warnings cli gate-engine *.mjs skills", - "lint:biome": "biome check --config-path biome/non-js.jsonc --formatter-enabled=false --assist-enabled=false --error-on-warnings .", - "lint:regex": "biome lint --config-path biome/regex.jsonc --diagnostic-level=error cli gate-engine", "lint:anti-slop": "node cli/index.mts anti-slop check", "lint:structure": "eslint cli gate-engine", "benchmarks:check": "bun gate-engine/eval/cli.mts check", @@ -70,15 +68,14 @@ "benchmarks:typecheck": "tsc -p gate-engine/eval/tsconfig.json", "comments:eval": "node gate-engine/comment-firewall/eval/run.mts", "search-eval:check": "node gate-engine/search-tool/eval/eval.mts --fail", - "format": "oxfmt --write 'cli/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'gate-engine/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'tsconfig/**/*.{json,jsonc}' 'biome/**/*.{json,jsonc}' 'skills/**/*.mjs' .co-occurrence-allowlist.json .fallowrc.jsonc .oxfmtrc.json biome.jsonc eslint.config.mjs guard.config.example.json guard.config.json package.json search-code.config.json tsconfig.build.json tsconfig.json vitest.config.mjs vitest.e2e.config.mjs vitest.setup.mjs", - "format:check": "oxfmt --check 'cli/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'gate-engine/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'tsconfig/**/*.{json,jsonc}' 'biome/**/*.{json,jsonc}' 'skills/**/*.mjs' .co-occurrence-allowlist.json .fallowrc.jsonc .oxfmtrc.json biome.jsonc eslint.config.mjs guard.config.example.json guard.config.json package.json search-code.config.json tsconfig.build.json tsconfig.json vitest.config.mjs vitest.e2e.config.mjs vitest.setup.mjs", + "format": "oxfmt --write 'cli/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'gate-engine/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'tsconfig/**/*.{json,jsonc}' 'biome/**/*.{json,jsonc}' 'skills/**/*.mjs' .co-occurrence-allowlist.json .fallowrc.jsonc .oxfmtrc.json eslint.config.mjs guard.config.example.json guard.config.json package.json search-code.config.json tsconfig.build.json tsconfig.json vitest.config.mjs vitest.e2e.config.mjs vitest.setup.mjs", + "format:check": "oxfmt --check 'cli/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'gate-engine/**/*.{ts,tsx,js,jsx,mts,mjs,css,json,jsonc}' 'tsconfig/**/*.{json,jsonc}' 'biome/**/*.{json,jsonc}' 'skills/**/*.mjs' .co-occurrence-allowlist.json .fallowrc.jsonc .oxfmtrc.json eslint.config.mjs guard.config.example.json guard.config.json package.json search-code.config.json tsconfig.build.json tsconfig.json vitest.config.mjs vitest.e2e.config.mjs vitest.setup.mjs", "typecheck": "tsc -p tsconfig.json", "prepare": "husky", "guard:freeze": "node gate-engine/ratchets/folder-fanout.mjs freeze && node gate-engine/ratchets/size-disable.mjs freeze", "build": "tsc -p tsconfig.build.json && node scripts/copy-dist-assets.mjs" }, "devDependencies": { - "@biomejs/biome": "^2.5.6", "@types/node": "^25.9.3", "@vitest/coverage-v8": "^4.1.10", "husky": "^9.1.7", From ff28c076af50eaf1fbd037309b57d92704272a74 Mon Sep 17 00:00:00 2001 From: norvalbv Date: Tue, 18 Aug 2026 17:31:39 +0100 Subject: [PATCH 3/3] fix(lint): cover rebased comment fixture --- dist/oxc/oxlint.devkit-lint.json | 6 + .../README.md | 15 +- .../benchmark.mjs | 13 +- .../results.json | 278 +++++++++--------- docs/decisions/oxc-toolchain-migration.md | 2 +- oxc/oxlint.devkit-lint.json | 6 + 6 files changed, 170 insertions(+), 150 deletions(-) diff --git a/dist/oxc/oxlint.devkit-lint.json b/dist/oxc/oxlint.devkit-lint.json index c033fa24..dead088a 100644 --- a/dist/oxc/oxlint.devkit-lint.json +++ b/dist/oxc/oxlint.devkit-lint.json @@ -26,6 +26,12 @@ "rules": { "eslint/no-unused-vars": "allow" } + }, + { + "files": ["**/comment-firewall/__tests__/detect.test.mts"], + "rules": { + "eslint/no-useless-concat": "allow" + } } ] } diff --git a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md index 25a09e1b..b46af942 100644 --- a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md +++ b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/README.md @@ -12,9 +12,9 @@ The same Devkit lint command was run ten times before and after the change. The | What you feel | Before | After | Improvement | | --- | ---: | ---: | ---: | -| CPU occupied by the check | 2.32 seconds | 0.16 seconds | **93.1% less CPU** | -| Time waiting for it to finish | 0.38 seconds | 0.09 seconds | **75.8% faster** | -| Peak memory used by the command and children | 206 MiB | 97 MiB | 53.1% lower | +| CPU occupied by the check | 2.41 seconds | 0.18 seconds | **92.5% less CPU** | +| Time waiting for it to finish | 0.41 seconds | 0.10 seconds | **74.5% faster** | +| Peak memory used by the command and children | 208 MiB | 91 MiB | 56.4% lower | CPU is the reason for the decision. Memory is only a safety measure here; an increase would have been acceptable if the CPU saving still made the agent loop materially faster. @@ -77,9 +77,10 @@ work, not an invisible second lint lane. ## Measurement protocol -The control is a clean archive of `origin/main` at `8f562e02c7bef763b2aee1903040d05c67e22f70`, +The control is a clean archive of `origin/main` at `76383ff117bcd64f10cceb0d8cbea12e4d8df3a2`, running its original `bun run lint`. The candidate is this worktree running the new command of the -same name. Both include normal command startup; installed dependencies are shared and excluded. +same name. Both include normal command startup; each side installs its locked dependencies before +timing, and that setup time is excluded. The machine was macOS arm64 with Node 24.19.0, Bun 1.3.1, and ten logical CPUs. There were three warm-up runs, then ten measured pairs in alternating order. CPU is user plus system time collected @@ -87,8 +88,8 @@ by `/usr/bin/time -lp`; the memory figure samples the command and its children e samples and summaries are in [results.json](results.json); the reproducer is [benchmark.mjs](benchmark.mjs). -The slowest measured candidate run still used only 0.18 seconds CPU and took 0.09 seconds wall -time, compared with 2.36 seconds CPU and 0.38 seconds wall time for the slowest old run. +The slowest measured candidate run still used only 0.18 seconds CPU and took 0.11 seconds wall +time, compared with 2.48 seconds CPU and 0.44 seconds wall time for the slowest old run. ## Acceptance checks diff --git a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/benchmark.mjs b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/benchmark.mjs index 29de4ec3..bba8f594 100644 --- a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/benchmark.mjs +++ b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/benchmark.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { execFileSync, spawn, spawnSync } from 'node:child_process'; -import { mkdtempSync, mkdirSync, rmSync, symlinkSync } from 'node:fs'; +import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -142,7 +142,13 @@ function prepareControl(tempRoot) { }); const extracted = spawnSync('tar', ['-x', '-C', control], { input: archive }); if (extracted.status !== 0) throw new Error(`control archive extraction failed: ${extracted.stderr}`); - symlinkSync(join(REPO_ROOT, 'node_modules'), join(control, 'node_modules'), 'dir'); + const installed = spawnSync('bun', ['install', '--frozen-lockfile', '--ignore-scripts'], { + cwd: control, + encoding: 'utf8', + }); + if (installed.status !== 0) { + throw new Error(`control dependency install failed: ${installed.stdout}\n${installed.stderr}`); + } return control; } @@ -184,7 +190,8 @@ async function main() { order: 'alternating control-first/candidate-first', control: 'clean origin/main archive with its original bun run lint script', candidate: 'this candidate worktree with its proposed bun run lint script', - dependencies: 'shared installed node_modules, excluded from timing', + dependencies: + 'each side has its locked dependencies installed before timing; install time excluded', cpu: '/usr/bin/time -lp aggregate user + sys; primary decision metric', rss: '10ms sampling; sum RSS of /usr/bin/time wrapper and all descendants', }, diff --git a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json index b213b563..1cbcfa65 100644 --- a/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json +++ b/docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/results.json @@ -1,10 +1,10 @@ { "schemaVersion": 1, - "recordedAt": "2026-08-18T16:04:43.049Z", + "recordedAt": "2026-08-18T16:29:01.692Z", "source": { "controlRef": "origin/main", - "controlCommit": "8f562e02c7bef763b2aee1903040d05c67e22f70", - "candidateCommit": "4dc191cb9bfa9cbf910b9f8c5a6d47e908593799" + "controlCommit": "76383ff117bcd64f10cceb0d8cbea12e4d8df3a2", + "candidateCommit": "c158bafee68421f67cb5b0cd1620a24a014d2026" }, "host": { "uname": "Darwin Benji-mac 25.5.0 Darwin Kernel Version 25.5.0: Mon Apr 27 20:41:26 PDT 2026; root:xnu-12377.121.6~2/RELEASE_ARM64_T8132 arm64", @@ -18,7 +18,7 @@ "order": "alternating control-first/candidate-first", "control": "clean origin/main archive with its original bun run lint script", "candidate": "this candidate worktree with its proposed bun run lint script", - "dependencies": "shared installed node_modules, excluded from timing", + "dependencies": "each side has its locked dependencies installed before timing; install time excluded", "cpu": "/usr/bin/time -lp aggregate user + sys; primary decision metric", "rss": "10ms sampling; sum RSS of /usr/bin/time wrapper and all descendants" }, @@ -29,220 +29,220 @@ "samples": { "control": [ { - "wallMs": 383.094417, - "cpuMs": 2330, - "userMs": 360, - "systemMs": 1970, - "treePeakRssBytes": 216825856, - "directPeakRssBytes": 165445632 + "wallMs": 426.33308299999953, + "cpuMs": 2420, + "userMs": 390, + "systemMs": 2029.9999999999998, + "treePeakRssBytes": 218218496, + "directPeakRssBytes": 166821888 }, { - "wallMs": 354.940208, - "cpuMs": 2290, - "userMs": 330, - "systemMs": 1960, - "treePeakRssBytes": 217382912, - "directPeakRssBytes": 166051840 + "wallMs": 412.34375, + "cpuMs": 2430, + "userMs": 370, + "systemMs": 2060, + "treePeakRssBytes": 218710016, + "directPeakRssBytes": 167100416 }, { - "wallMs": 378.64416600000004, - "cpuMs": 2300, - "userMs": 340, - "systemMs": 1960, - "treePeakRssBytes": 216547328, - "directPeakRssBytes": 165511168 + "wallMs": 398.941417, + "cpuMs": 2370, + "userMs": 360, + "systemMs": 2009.9999999999998, + "treePeakRssBytes": 218316800, + "directPeakRssBytes": 166805504 }, { - "wallMs": 381.06408399999964, - "cpuMs": 2340, - "userMs": 340, - "systemMs": 2000, - "treePeakRssBytes": 216121344, - "directPeakRssBytes": 164757504 + "wallMs": 404.99554100000023, + "cpuMs": 2390, + "userMs": 350, + "systemMs": 2040, + "treePeakRssBytes": 218038272, + "directPeakRssBytes": 166494208 }, { - "wallMs": 375.98054200000024, - "cpuMs": 2330, - "userMs": 340, - "systemMs": 1990, - "treePeakRssBytes": 216072192, - "directPeakRssBytes": 164757504 + "wallMs": 424.7429169999996, + "cpuMs": 2480, + "userMs": 380, + "systemMs": 2100, + "treePeakRssBytes": 218251264, + "directPeakRssBytes": 166690816 }, { - "wallMs": 378.3156250000002, - "cpuMs": 2360, - "userMs": 350, - "systemMs": 2009.9999999999998, - "treePeakRssBytes": 216645632, - "directPeakRssBytes": 165380096 + "wallMs": 432.8482090000007, + "cpuMs": 2450, + "userMs": 380, + "systemMs": 2070, + "treePeakRssBytes": 218431488, + "directPeakRssBytes": 166887424 }, { - "wallMs": 373.86091599999963, - "cpuMs": 2300, - "userMs": 340, - "systemMs": 1960, - "treePeakRssBytes": 216367104, - "directPeakRssBytes": 165019648 + "wallMs": 405.25104100000044, + "cpuMs": 2390, + "userMs": 360, + "systemMs": 2029.9999999999998, + "treePeakRssBytes": 218775552, + "directPeakRssBytes": 167264256 }, { - "wallMs": 358.9548329999998, - "cpuMs": 2320, - "userMs": 330, - "systemMs": 1990, - "treePeakRssBytes": 216842240, - "directPeakRssBytes": 165478400 + "wallMs": 401.6501249999992, + "cpuMs": 2380, + "userMs": 350, + "systemMs": 2029.9999999999998, + "treePeakRssBytes": 217956352, + "directPeakRssBytes": 166477824 }, { - "wallMs": 376.9319169999999, - "cpuMs": 2320, - "userMs": 340, - "systemMs": 1980, - "treePeakRssBytes": 216285184, - "directPeakRssBytes": 164872192 + "wallMs": 395.0868330000012, + "cpuMs": 2400, + "userMs": 370, + "systemMs": 2029.9999999999998, + "treePeakRssBytes": 219086848, + "directPeakRssBytes": 167624704 }, { - "wallMs": 348.82737499999985, - "cpuMs": 2280, - "userMs": 320, - "systemMs": 1960, - "treePeakRssBytes": 216449024, - "directPeakRssBytes": 165101568 + "wallMs": 441.24483300000065, + "cpuMs": 2440, + "userMs": 400, + "systemMs": 2040, + "treePeakRssBytes": 218398720, + "directPeakRssBytes": 166952960 } ], "candidate": [ { - "wallMs": 91.54825000000028, - "cpuMs": 170, - "userMs": 50, + "wallMs": 106.0815419999999, + "cpuMs": 180, + "userMs": 60, "systemMs": 120, - "treePeakRssBytes": 106233856, - "directPeakRssBytes": 94388224 + "treePeakRssBytes": 97320960, + "directPeakRssBytes": 97320960 }, { - "wallMs": 89.032917, - "cpuMs": 160, - "userMs": 50, - "systemMs": 110, - "treePeakRssBytes": 106381312, - "directPeakRssBytes": 94535680 + "wallMs": 103.86833399999978, + "cpuMs": 180, + "userMs": 60, + "systemMs": 120, + "treePeakRssBytes": 95256576, + "directPeakRssBytes": 95256576 }, { - "wallMs": 90.5981670000001, + "wallMs": 88.01283299999977, "cpuMs": 170, - "userMs": 50, - "systemMs": 120, - "treePeakRssBytes": 95862784, - "directPeakRssBytes": 95862784 + "userMs": 60, + "systemMs": 110, + "treePeakRssBytes": 95338496, + "directPeakRssBytes": 95338496 }, { - "wallMs": 91.85845900000004, - "cpuMs": 160, - "userMs": 50, - "systemMs": 110, - "treePeakRssBytes": 94912512, - "directPeakRssBytes": 94912512 + "wallMs": 105.12704099999974, + "cpuMs": 180, + "userMs": 60, + "systemMs": 120, + "treePeakRssBytes": 95977472, + "directPeakRssBytes": 95977472 }, { - "wallMs": 91.68775000000005, - "cpuMs": 170, - "userMs": 50, + "wallMs": 102.34370899999976, + "cpuMs": 180, + "userMs": 60, "systemMs": 120, - "treePeakRssBytes": 94404608, - "directPeakRssBytes": 94404608 + "treePeakRssBytes": 95223808, + "directPeakRssBytes": 95223808 }, { - "wallMs": 89.10645900000054, - "cpuMs": 160, - "userMs": 50, - "systemMs": 110, - "treePeakRssBytes": 104316928, - "directPeakRssBytes": 93618176 + "wallMs": 104.24091600000065, + "cpuMs": 180, + "userMs": 60, + "systemMs": 120, + "treePeakRssBytes": 93405184, + "directPeakRssBytes": 93405184 }, { - "wallMs": 89.1973339999995, + "wallMs": 102.78479099999913, "cpuMs": 180, "userMs": 60, "systemMs": 120, - "treePeakRssBytes": 102367232, - "directPeakRssBytes": 93634560 + "treePeakRssBytes": 101777408, + "directPeakRssBytes": 95092736 }, { - "wallMs": 91.27450000000044, - "cpuMs": 160, - "userMs": 50, - "systemMs": 110, - "treePeakRssBytes": 106594304, - "directPeakRssBytes": 94732288 + "wallMs": 110.55375000000004, + "cpuMs": 180, + "userMs": 60, + "systemMs": 120, + "treePeakRssBytes": 95272960, + "directPeakRssBytes": 95272960 }, { - "wallMs": 92.46587499999987, - "cpuMs": 160, - "userMs": 50, - "systemMs": 110, - "treePeakRssBytes": 100679680, - "directPeakRssBytes": 94306304 + "wallMs": 105.84124999999949, + "cpuMs": 180, + "userMs": 60, + "systemMs": 120, + "treePeakRssBytes": 96092160, + "directPeakRssBytes": 96092160 }, { - "wallMs": 91.18983300000036, - "cpuMs": 160, - "userMs": 50, - "systemMs": 110, - "treePeakRssBytes": 94519296, - "directPeakRssBytes": 94519296 + "wallMs": 103.44437500000095, + "cpuMs": 180, + "userMs": 60, + "systemMs": 120, + "treePeakRssBytes": 93814784, + "directPeakRssBytes": 93814784 } ] }, "summary": { "control": { "wallMs": { - "median": 376.45622950000006, - "p95": 383.094417 + "median": 408.7973955000002, + "p95": 441.24483300000065 }, "cpuMs": { - "median": 2320, - "p95": 2360 + "median": 2410, + "p95": 2480 }, "userMs": { - "median": 340, - "p95": 360 + "median": 370, + "p95": 400 }, "systemMs": { - "median": 1975, - "p95": 2009.9999999999998 + "median": 2035, + "p95": 2100 }, "treePeakRssBytes": { - "median": 216498176, - "p95": 217382912 + "median": 218357760, + "p95": 219086848 }, "directPeakRssBytes": { - "median": 165240832, - "p95": 166051840 + "median": 166854656, + "p95": 167624704 } }, "candidate": { "wallMs": { - "median": 91.2321665000004, - "p95": 92.46587499999987 + "median": 104.05462500000021, + "p95": 110.55375000000004 }, "cpuMs": { - "median": 160, + "median": 180, "p95": 180 }, "userMs": { - "median": 50, + "median": 60, "p95": 60 }, "systemMs": { - "median": 110, + "median": 120, "p95": 120 }, "treePeakRssBytes": { - "median": 101523456, - "p95": 106594304 + "median": 95305728, + "p95": 101777408 }, "directPeakRssBytes": { - "median": 94461952, - "p95": 95862784 + "median": 95264768, + "p95": 97320960 } } } diff --git a/docs/decisions/oxc-toolchain-migration.md b/docs/decisions/oxc-toolchain-migration.md index cdc7fcef..6fb91258 100644 --- a/docs/decisions/oxc-toolchain-migration.md +++ b/docs/decisions/oxc-toolchain-migration.md @@ -26,4 +26,4 @@ created: 2026-08-15 - 2026-08-16 — sc-1679 proves Oxfmt 0.63.0 over Devkit's exact 558-file Biome formatting scope. The corrected migration changes seven TypeScript files with formatter-only hunks, is byte-idempotent on pass two, and keeps JSON/JSONC/package ordering stable through explicit overrides. Ten paired samples show the direct pinned binary cuts full-scope median CPU 72.6% (0.8709s to 0.2382s), wall 37.8% (0.1997s to 0.1242s), and process-tree RSS 24.1% (143.7 to 109.1 MiB); a one-file single-thread check-mode proxy cuts CPU 8.1% but increases wall/RSS. Devkit adopts direct Oxfmt for its own formatting, CI, and staged self-host path, retains Biome for lint and consumer configs/hooks, and keeps devkit oxc fmt out of the hot staged path because Node-wrapper startup measured 0.1113s CPU and 137.0 MiB there. - 2026-08-16 — sc-1676 vendors anti-slop commit 446268e5d15baa968eaec669ff65358d36ae6259 with @oxlint/plugins@1.78.0 into the managed Oxc configuration and adds an explicit, deterministic create/check/inspect/prune baseline lifecycle. Normal checks stay read-only and reject only unbaselined error-severity findings; prune can only delete absent debt or reduce duplicate counts. Rule severity and scoped overrides remain native Oxlint config so anti-slop composes with other Oxc rules. - 2026-08-16 — sc-1681 closes Devkit dogfooding with the mixed ownership boundary recorded in the [assembled benchmark](../benchmarks/experiments/2026-08-16-oxc-devkit-dogfood/README.md). Oxfmt and anti-slop/Oxlint are adopted; Biome lint, ESLint topology, and TypeScript remain because sc-1677, sc-1678, and sc-1680 found concrete diagnostic or semantic gaps despite faster candidates. Devkit now commits its managed Oxc/plugin bytes and 1,677-finding baseline, gates the exact Git index locally, rejects base-to-candidate baseline growth in CI, and checks all managed state through self-host doctor. The measured local agent segment is slightly faster while adding the policy (-3.0% median CPU, -2.7% median process-tree RSS), while full CI is +39.4% median CPU because it deliberately adds anti-slop and typechecks its vendored source. Frink adoption therefore requires its own assembled benchmark; individual microbenchmark speed is not enough to remove an incumbent owner. -- 2026-08-18 — sc-1787 adopts native Oxlint as Devkit's own ordinary JS/TS lint owner after replacing the ambient Biome recommended preset with an explicit 155-rule correctness/suspicious/perf policy plus React hooks. The pinned Node 24.19 paired experiment compares the actual old and new `bun run lint` commands: median CPU falls 93.1% (2.320s to 0.160s), wall 75.8% (0.376s to 0.091s), and process-tree RSS 53.1% (206.5 to 96.8 MiB). Following the maintainer's hard-cutover direction, Devkit's local lint, hook, and CI now invoke Oxlint only: Biome's JSON/CSS diagnostics, production `useTopLevelRegex`, and static import assist are explicitly retired rather than carried as small fallback passes. Formatting remains Oxfmt, topology ESLint, anti-slop its baseline-aware Oxlint lane, and tsc the type checker. Distributed Biome presets remain published only as temporary consumer compatibility contracts; no Devkit self-host process executes them, and their removal belongs to the separately measured consumer/Frink port. Evidence: docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/. +- 2026-08-18 — sc-1787 adopts native Oxlint as Devkit's own ordinary JS/TS lint owner after replacing the ambient Biome recommended preset with an explicit 155-rule correctness/suspicious/perf policy plus React hooks. The pinned Node 24.19 paired experiment compares the actual old and new `bun run lint` commands: median CPU falls 92.5% (2.410s to 0.180s), wall 74.5% (0.409s to 0.104s), and process-tree RSS 56.4% (208.2 to 90.9 MiB). Following the maintainer's hard-cutover direction, Devkit's local lint, hook, and CI now invoke Oxlint only: Biome's JSON/CSS diagnostics, production `useTopLevelRegex`, and static import assist are explicitly retired rather than carried as small fallback passes. Formatting remains Oxfmt, topology ESLint, anti-slop its baseline-aware Oxlint lane, and tsc the type checker. Distributed Biome presets remain published only as temporary consumer compatibility contracts; no Devkit self-host process executes them, and their removal belongs to the separately measured consumer/Frink port. Evidence: docs/benchmarks/experiments/2026-08-18-oxlint-native-devkit-adoption/. diff --git a/oxc/oxlint.devkit-lint.json b/oxc/oxlint.devkit-lint.json index c033fa24..dead088a 100644 --- a/oxc/oxlint.devkit-lint.json +++ b/oxc/oxlint.devkit-lint.json @@ -26,6 +26,12 @@ "rules": { "eslint/no-unused-vars": "allow" } + }, + { + "files": ["**/comment-firewall/__tests__/detect.test.mts"], + "rules": { + "eslint/no-useless-concat": "allow" + } } ] }