diff --git a/docs/benchmarks/experiments/2026-08-16-topology-guard/README.md b/docs/benchmarks/experiments/2026-08-16-topology-guard/README.md new file mode 100644 index 0000000..c0eda5d --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-16-topology-guard/README.md @@ -0,0 +1,154 @@ +# Standalone topology guard prototype — 2026-08-16 + +**Story:** Shortcut sc-1678 + +**Status:** one-off migration experiment, not a recurring performance metric + +**Verdict:** **retain ESLint; do not ship `guard-topology` from this prototype** + +The direct filesystem walker is materially faster and lighter than the current +`guard-structure -> ESLint -> eslint-plugin-project-structure` process. It does not meet the +pre-registered adoption boundary, however: it misses the incumbent import-wall responsibility, +cannot report an illegal empty directory, and has no config input for Electron's six hand-written +folder trees. Speed does not override these coverage gaps. + +This experiment makes no claim that the prototype is Oxc-native. Both the current command and the +candidate are Devkit-orchestrated JavaScript processes; the candidate directly walks the filesystem. + +## Decision + +Keep the current ownership split: + +- config-driven placement, naming, and required-sibling rules remain on `guard-structure`, which + runs Devkit's packaged ESLint and `eslint-plugin-project-structure`; +- Electron's six hand-written folder trees and its import walls remain in the consumer-side ESLint + preset; +- generated structure baselines and permanent exemptions keep their current semantics; and +- no install, doctor, hook, package dependency, or command registration changes in this story. + +A later direct runner can be reconsidered when it has a real import parser/resolver with the current +wall's import forms and alias behavior, emits directory-level violations, and repeats this paired +benchmark at coverage parity. The faster placement-only prototype is not being shipped as a hybrid: +that would retain ESLint for walls while adding a second gate and diagnostic surface. + +The pre-implementation critique and rejected alternatives are recorded in +[`feature-critique.md`](feature-critique.md). This outcome is an implementation note under the +existing [`oxc-toolchain-migration`](../../../decisions/oxc-toolchain-migration.md) Target, not a new +architectural axis. + +## Headline benchmark + +The fixture has 481 governed files across 80 component directories: TypeScript, TSX, CSS, HTML, +SVG, and an arbitrary extension. The current lane uses a packed `@norvalbv/devkit@0.51.1` installed +inside the disposable fixture, so the plugin observes the same `node_modules` layout as a real +consumer. Dependencies and packaging are completed before timing. + +Three warm-ups were discarded. Each table cell is median / nearest-rank p95 over 20 measured runs; +the order alternated current-first and candidate-first. CPU is aggregate user + system from +`/usr/bin/time -lp`. Process-tree RSS is sampled every 10 ms by summing the wrapper and all +descendants, with the direct child's wait4 peak as a floor. + +### Clean full-tree lane + +| Runner | Wall | CPU | Process-tree peak RSS | +| --- | ---: | ---: | ---: | +| Current packaged ESLint chain | 253.2 / 282.6 ms | 490 / 520 ms | 116.1 / 116.6 MiB | +| Direct walker prototype | 90.5 / 106.2 ms | 120 / 130 ms | 79.2 / 79.6 MiB | +| Median reduction | **64.3%** | **75.5%** | **31.8%** | + +### One placement violation lane + +Both runners find exactly one `src/loose.ts` placement violation before timing this lane. + +| Runner | Wall | CPU | Process-tree peak RSS | +| --- | ---: | ---: | ---: | +| Current packaged ESLint chain | 264.3 / 277.3 ms | 500 / 530 ms | 116.1 / 116.7 MiB | +| Direct walker prototype | 87.6 / 97.8 ms | 120 / 130 ms | 79.3 / 79.5 MiB | +| Median reduction | **66.9%** | **76.0%** | **31.7%** | + +The latency and CPU win is large enough to justify a future implementation, but it is not adoption +evidence without responsibility parity. The candidate also reports a generic path violation where +the plugin currently explains allowed names or the missing sibling, so diagnostic-message parity +would still need deliberate UX work. + +## Coverage and exact diagnostic differences + +Each row uses a fresh Node process and a two-component fixture. “Current command” means the packaged +generic `guard-structure` folder rule. Import walls are tested separately through the broader +incumbent ESLint `independent-modules` owner because the generic command does not compile +`structure.walls` today. + +| Case | Expected | Current command | Direct candidate | Difference | +| --- | --- | ---: | ---: | --- | +| Clean tree | clean | 0 | 0 | exact | +| Folder/file placement | violation | 1 | 1 | same path/count; current message lists allowed names, candidate is generic | +| Required `index.ts` sibling | violation | 1 | 1 | same path/count; current names the missing sibling, candidate is generic | +| Folder naming | violation | 1 | 1 | same path/count; current lists allowed folder pattern, candidate is generic | +| Root CSS file | violation | 0 | 1 | candidate expands coverage; current source-extension manifest never invokes the rule for it | +| Root HTML file | violation | 0 | 1 | candidate expands coverage | +| Root SVG asset | violation | 0 | 1 | candidate expands coverage | +| Arbitrary extension | violation | 0 | 1 | candidate expands coverage | +| Illegal empty directory | violation | 0 | 0 | **shared gap; candidate walker discards directory-only entries** | +| Ignored directory | clean | 0 | 0 | exact | +| Generated debt baseline | clean | 0 | 0 | exact | +| Permanent exemption | clean | 0 | 0 | exact | +| Cross-feature import wall | violation | 1 via incumbent ESLint wall | 0 | **candidate parity failure** | +| Electron's six hand-written folder trees | violations | incumbent consumer ESLint preset | no config input | **candidate responsibility gap; Electron guard config has no `structure` block** | + +The CSS/HTML/asset/arbitrary-extension rows are a useful correction to the earlier migration +hypothesis: the baseline walker sees these files, but the generic ESLint gate's generated +`files: src/**/*.{ts,tsx}` manifest does not lint them. The direct walker therefore expands those +classes rather than merely preserving the current command. Existing baselines still suppress the +committed debt in the candidate fixture, but new arbitrary-file enforcement would be a behavior +change requiring its own rollout audit. + +The import-wall fixture proves the incumbent rule rejects +`src/feature-a/index.js -> src/feature-b/internal.js` and includes both resolved paths in its +diagnostic. The candidate has no source parser or resolver at all. A regex-only substitute would +also lose `require`, dynamic import, re-export, mock, query-suffix, and alias handling, so it was not +treated as a viable parity implementation. + +Static ownership inspection found a second Electron-specific gap beyond import walls. The shipped +Electron preset constructs six `createFolderStructure` policies for renderer, main, shared, +preload, socket, and Vercel trees directly in `templates/electron/eslint.config.mjs`; its +`guard.config.json` has no `structure.trees` representation. The prototype reads only those config +trees, so it cannot even ingest this incumbent folder-topology responsibility. A replacement must +first represent or migrate that preset and then add parity fixtures for all six trees. + +## Reproduction + +From a clean Devkit checkout on macOS with dependencies installed: + +```bash +bun run build +cd docs/benchmarks/experiments/2026-08-16-topology-guard +node benchmark.mjs --output results.json +``` + +The harness: + +1. creates a disposable fixture; +2. packs the checkout and installs Devkit into that fixture without lifecycle scripts; +3. generates the same deterministic 481-file corpus for both runners; +4. runs three discarded warm-ups and 20 alternating samples per lane; +5. runs the coverage cases in fresh processes; +6. records raw samples, diagnostics, host metadata, and the source commit in + [`results.json`](results.json); and +7. deletes the package and fixture directories after the run. + +The committed raw result was captured on Darwin 25.5.0 arm64, 10 logical CPUs, Node 24.19.0, from +source commit `4d624d059c8974aa58e0f3a27277cd6fd90383a4`. Host load was not isolated, so absolute p95 values +should not be generalized to another machine. Alternating paired lanes and the size of the median +deltas make the directional result clear; they do not rescue the coverage failure. + +## Revisit conditions + +Do not reopen the migration on startup-speed claims alone. Re-run this experiment only after a +candidate can demonstrate all of the following on the fixture and then on Frink: + +- directory-level findings, including empty illegal directories; +- a config representation and parity fixtures for all six hand-written Electron folder trees; +- the current import forms, path aliases, query suffixes, and first-match/baseline wall semantics; +- collision-safe generated debt and permanent exemption behavior; +- stable, actionable diagnostics for placement, naming, and required siblings; and +- the same or better process-tree CPU, wall, and RSS result at full coverage parity. diff --git a/docs/benchmarks/experiments/2026-08-16-topology-guard/benchmark.mjs b/docs/benchmarks/experiments/2026-08-16-topology-guard/benchmark.mjs new file mode 100644 index 0000000..16b486d --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-16-topology-guard/benchmark.mjs @@ -0,0 +1,294 @@ +#!/usr/bin/env node + +import { execFileSync, spawn, spawnSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } 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"; +import { CASES, createFixture } from "./fixture.mjs"; +import { runImportWallFixture } from "./import-wall-current.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(HERE, "..", "..", "..", ".."); +const WARMUPS = 3; +const SAMPLES = 20; +const TIME_BIN = "/usr/bin/time"; +const PS_BIN = "/bin/ps"; + +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(script, cwd, args) { + const started = performance.now(); + const child = spawn(TIME_BIN, ["-lp", process.execPath, script, ...args], { + cwd, + 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, + }; +} + +const 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]; +}; +const p95 = (values) => [...values].sort((a, b) => a - b)[Math.ceil(values.length * 0.95) - 1]; +const summarize = (samples) => + 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 resetFixture(root) { + for (const relativePath of [ + "src", + "eslint", + "guard.config.json", + "projectStructure.cache.json", + ]) { + rmSync(join(root, relativePath), { recursive: true, force: true }); + } +} + +function runCurrentCase(root, bin) { + const result = spawnSync(process.execPath, [bin, "gate"], { cwd: root, encoding: "utf8" }); + const text = `${result.stdout ?? ""}${result.stderr ?? ""}`; + return { + code: result.status ?? 2, + errorCount: Number(text.match(/✖\s+(\d+)\s+problem/)?.[1] ?? 0), + text, + }; +} + +function runCandidateCase(root) { + const output = execFileSync(process.execPath, [join(HERE, "candidate.mjs"), "--json"], { + cwd: root, + encoding: "utf8", + }); + return JSON.parse(output); +} + +async function coverageMatrix(root, currentBin) { + const rows = []; + for (const fixtureCase of CASES) { + resetFixture(root); + createFixture(root, { caseId: fixtureCase.id, componentCount: 2 }); + const current = runCurrentCase(root, currentBin); + const candidate = runCandidateCase(root); + rows.push({ + ...fixtureCase, + current: { code: current.code, errorCount: current.errorCount, text: current.text ?? "" }, + candidate: { code: candidate.code, diagnostics: candidate.diagnostics }, + }); + } + + resetFixture(root); + mkdirSync(join(root, "src", "feature-a"), { recursive: true }); + mkdirSync(join(root, "src", "feature-b"), { recursive: true }); + writeFileSync(join(root, "src", "feature-a", "index.js"), "import '../feature-b/internal.js';\n"); + writeFileSync(join(root, "src", "feature-b", "internal.js"), "export const internal = true;\n"); + writeFileSync( + join(root, "guard.config.json"), + `${JSON.stringify({ + scanRoots: ["src"], + sourceExtensions: ["js"], + structure: { + trees: [], + walls: [{ from: "src/feature-a/**", disallow: "src/feature-b/**" }], + }, + })}\n`, + ); + const current = await runImportWallFixture(root); + const candidate = runCandidateCase(root); + rows.push({ + id: "import-wall", + expectedViolation: true, + currentBroaderEslintOwner: current, + candidate: { code: candidate.code, diagnostics: candidate.diagnostics }, + }); + return rows; +} + +function installPackedDevkit(root, packRoot) { + writeFileSync(join(root, "package.json"), '{"private":true,"type":"module"}\n'); + const packed = JSON.parse( + execFileSync("npm", ["pack", "--json", "--pack-destination", packRoot], { + cwd: REPO_ROOT, + encoding: "utf8", + }), + ); + const tarball = join(packRoot, packed[0].filename); + execFileSync("bun", ["add", "--cwd", root, tarball, "--ignore-scripts"], { stdio: "ignore" }); + const packageRoot = join(root, "node_modules", "@norvalbv", "devkit"); + const installedPackage = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")); + const driver = join(root, "current-driver.mjs"); + writeFileSync( + driver, + "import { runStructureGate } from './node_modules/@norvalbv/devkit/dist/gate-engine/structure/run.mjs';\nawait runStructureGate(process.cwd());\n", + ); + return { + bin: join(packageRoot, "dist", "gate-engine", "structure", "run.mjs"), + driver, + version: installedPackage.version, + }; +} + +async function benchmarkLane(root, caseId, currentScript, candidateScript) { + resetFixture(root); + createFixture(root, { caseId, componentCount: 80 }); + for (let index = 0; index < WARMUPS; index += 1) { + await measure(currentScript, root, []); + await measure(candidateScript, root, ["--bench"]); + } + const samples = { current: [], candidate: [] }; + for (let index = 0; index < SAMPLES; index += 1) { + const order = + index % 2 === 0 + ? [ + ["current", currentScript, []], + ["candidate", candidateScript, ["--bench"]], + ] + : [ + ["candidate", candidateScript, ["--bench"]], + ["current", currentScript, []], + ]; + for (const [name, script, args] of order) { + samples[name].push(await measure(script, root, args)); + } + } + return { + fixtureCase: caseId, + samples, + summary: { current: summarize(samples.current), candidate: summarize(samples.candidate) }, + }; +} + +async function main() { + const fixtureRoot = mkdtempSync(join(tmpdir(), "topology-benchmark-")); + const packRoot = mkdtempSync(join(tmpdir(), "topology-package-")); + const candidateScript = join(HERE, "candidate.mjs"); + try { + const installed = installPackedDevkit(fixtureRoot, packRoot); + const lanes = { + clean: await benchmarkLane(fixtureRoot, "clean", installed.driver, candidateScript), + placement: await benchmarkLane(fixtureRoot, "placement", installed.driver, candidateScript), + }; + const results = { + schemaVersion: 1, + recordedAt: new Date().toISOString(), + sourceCommit: execFileSync("git", ["rev-parse", "HEAD"], { + cwd: REPO_ROOT, + encoding: "utf8", + }).trim(), + host: { + uname: execFileSync("uname", ["-a"], { encoding: "utf8" }).trim(), + node: process.version, + logicalCpuCount: Number( + execFileSync("sysctl", ["-n", "hw.logicalcpu"], { encoding: "utf8" }).trim(), + ), + }, + protocol: { + warmups: WARMUPS, + samples: SAMPLES, + order: "alternating current-first/candidate-first", + fixture: { componentCount: 80, files: 481, roots: 1 }, + current: `packed @norvalbv/devkit@${installed.version} installed inside the fixture; driver invokes its runStructureGate API`, + rss: "10ms sampling; sum RSS of /usr/bin/time wrapper and all descendants, floored by wait4 direct-child peak RSS", + cpu: "/usr/bin/time -lp aggregate user + sys", + }, + coverage: await coverageMatrix(fixtureRoot, installed.bin), + lanes, + }; + const outputIndex = process.argv.indexOf("--output"); + if (outputIndex >= 0) { + const output = join(process.cwd(), process.argv[outputIndex + 1]); + writeFileSync(output, `${JSON.stringify(results, null, 2)}\n`); + } else { + console.log(JSON.stringify(results, null, 2)); + } + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + rmSync(packRoot, { recursive: true, force: true }); + } +} + +await main(); diff --git a/docs/benchmarks/experiments/2026-08-16-topology-guard/candidate.mjs b/docs/benchmarks/experiments/2026-08-16-topology-guard/candidate.mjs new file mode 100644 index 0000000..a7cc9c7 --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-16-topology-guard/candidate.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node + +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { resolveGuardConfig, resolveTreeExtensions } from "../../../../gate-engine/config.mts"; +import { makeBaselineLoaders } from "../../../../gate-engine/structure/load-baseline.mts"; +import { walkTree } from "../../../../gate-engine/structure/walk.mts"; + +export async function runCandidate(cwd = process.cwd()) { + try { + const config = resolveGuardConfig(cwd); + const loaders = makeBaselineLoaders(cwd); + const diagnostics = []; + for (const tree of config.structure?.trees ?? []) { + if (!tree.grammar || !tree.root || !tree.name) continue; + const absoluteRoot = join(cwd, tree.root); + if (!existsSync(absoluteRoot)) continue; + const ignored = new Set([ + ...(await loaders.loadBaseline(tree.name)), + ...(await loaders.loadExempt(tree.name)), + ]); + const extensions = resolveTreeExtensions(config, tree); + for (const relativePath of walkTree(tree, absoluteRoot, extensions)) { + if (!ignored.has(relativePath)) { + diagnostics.push({ tree: tree.name, path: `${tree.root}/${relativePath}` }); + } + } + } + return { code: diagnostics.length > 0 ? 1 : 0, diagnostics }; + } catch (error) { + return { + code: 2, + diagnostics: [], + error: error instanceof Error ? error.message : String(error), + }; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const result = await runCandidate(); + if (process.argv.includes("--json")) { + console.log(JSON.stringify(result)); + } else if (!process.argv.includes("--bench")) { + for (const diagnostic of result.diagnostics) { + console.error(`${diagnostic.path}: topology violation`); + } + if (result.error) console.error(`guard-topology candidate: ${result.error}`); + } + process.exit( + process.argv.includes("--json") || process.argv.includes("--bench") ? 0 : result.code, + ); +} diff --git a/docs/benchmarks/experiments/2026-08-16-topology-guard/feature-critique.md b/docs/benchmarks/experiments/2026-08-16-topology-guard/feature-critique.md new file mode 100644 index 0000000..c83d160 --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-16-topology-guard/feature-critique.md @@ -0,0 +1,95 @@ +# Feature critique: standalone filesystem topology guard + +**Story:** Shortcut sc-1678 + +**Decision boundary:** adopt only if the candidate preserves the entire topology responsibility and +measurably reduces CPU or agent-loop latency. Otherwise retain the ESLint path and record why. + +## Proposal + +Promote Devkit's existing config-driven filesystem walker from baseline generation into a direct +`guard-topology` command, then replace the current +`guard-structure -> ESLint -> eslint-plugin-project-structure` path. + +## Alternatives considered + +1. **Direct walker replacement.** Reuse `walkTree`, load the generated baseline and permanent + exemptions, and emit violations without starting ESLint. This has the smallest process-startup + and parser cost, but it is acceptable only at full responsibility parity. +2. **Hybrid direct placement plus ESLint import walls.** Move directory placement to the walker but + retain ESLint for `independent-modules`. This may reduce part of the cost, but it keeps both + runners and introduces two diagnostic/configuration surfaces during every agent loop. +3. **Retain ESLint for now.** Keep the current owner until a standalone implementation covers every + required class. This preserves behavior but leaves the measured startup and parser cost in place. + +The benchmark may prototype option 1, but the pre-registered acceptance rule prefers option 3 over +either a coverage regression or an unmeasured hybrid. + +## Prior art inspected before the prototype + +- [`runStructureGate`](../../../../gate-engine/structure/run.mts), the current packaged + ESLint orchestration and 0/1/2 contract; +- [`walkTree`](../../../../gate-engine/structure/walk.mts), the existing direct baseline walker; +- the shared grammar-to-ESLint compiler and token predicates in + [`gate-engine/structure`](../../../../gate-engine/structure/); +- the import-wall baseline generator's handling of resolved paths and loud failures in + [`generate-import-wall-baseline.mts`](../../../../cli/lib/generate/generate-import-wall-baseline.mts); +- the current structure-governance ownership and baseline model in + [`docs/structure-governance.md`](../../../structure-governance.md); +- the earlier [Frink lint-toolchain research](../2026-08-15-oxlint-js-plugin-frink/README.md) and + [`oxc-toolchain-migration`](../../../decisions/oxc-toolchain-migration.md) Target; and +- the installed `eslint-plugin-project-structure@3.14.3` implementation and documentation for + folder structure, `enforceExistence`, cache deduplication, arbitrary extensions, and + `independent-modules` import resolution. + +## Critique by lens + +### Feasibility and data-flow correctness + +The existing `walkTree` is a strong placement prototype because it already consumes the same +`guard.config.json` grammar and token predicates as the ESLint compiler. It is not yet a gate: + +- it returns only file paths intended for a grandfather baseline and deliberately drops directory + entries, so an empty illegal directory has no observable violation; +- its ignored-directory contract is basename membership, while ESLint consumes generated glob + patterns; equivalence must be tested rather than assumed; +- it has no import parser or resolver, so it cannot implement `independent-modules` import walls; +- the generated baseline and `exempt.mjs` are asynchronous module inputs to ESLint, whereas the + walker itself currently does not load or subtract either set; +- ESLint/plugin diagnostics are attached to linted files and can be deduplicated by the plugin's + cache. Exact message/count equality is therefore a stricter requirement than merely finding the + same broken subtree. + +### Runtime configurations + +The candidate must behave correctly for absent roots, multiple roots, source-extension overrides, +CSS, HTML, assets, arbitrary extensions, empty directories, ignored directories, generated debt +baselines, permanent exemptions, and malformed config/baseline inputs. Devkit's generic +`guard-structure` command currently compiles grammar trees only. Electron's six folder trees and its +import walls remain hand-written in the consumer-side ESLint configuration, while the Electron +guard config has no `structure` block. A replacement claim must cover both owners before ESLint can +be removed, rather than benchmarking only the generic command and silently narrowing “topology.” + +### UX and failure behavior + +A direct command could give agents materially earlier feedback and avoid loading a JS lint engine. +However, different path prefixes, duplicate counts, or missing required-sibling messages would make +existing baselines and remediation guidance misleading. The existing 0 clean / 1 violation / 2 +fail-open contract must remain stable. A hybrid would also make it unclear which command owns a +reported structural error. + +### Security and trust boundaries + +The walker operates inside consumer repositories, so every path must remain consumer-cwd-relative. +Import resolution is security-relevant: a naive regex over source text would miss dynamic imports, +re-exports, mocks, aliases, and resolver behavior that the current plugin handles. A partial parser is +not an acceptable substitute for the current wall. + +## Verdict before implementation + +**PROCEED WITH THE PROTOTYPE, WITH A RETAIN-BY-DEFAULT VERDICT.** Benchmark the direct walker on a +pinned, reproducible fixture and record its coverage matrix and exact diagnostics. Adopt it only if +all classes above pass and process-tree CPU or wall latency improves. Any import-wall, +empty-directory, baseline, ignore, arbitrary-file, or unrepresented Electron-preset gap is +independently sufficient to retain ESLint, regardless of the speed result. This is an implementation +note under the existing `oxc-toolchain-migration` Target, not a new decision axis. diff --git a/docs/benchmarks/experiments/2026-08-16-topology-guard/fixture.mjs b/docs/benchmarks/experiments/2026-08-16-topology-guard/fixture.mjs new file mode 100644 index 0000000..b094d95 --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-16-topology-guard/fixture.mjs @@ -0,0 +1,130 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +const write = (root, relativePath, contents = "") => { + const path = join(root, relativePath); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, contents); +}; + +const CONFIG = { + scanRoots: ["src"], + sourceExtensions: ["ts", "tsx"], + structure: { + trees: [ + { + name: "topology", + root: "src", + sourceExtensions: ["ts", "tsx"], + ignoredDirs: ["ignored"], + grammar: { + files: ["index.ts"], + recurse: "component", + rules: { + component: { + folderName: "{pascal_dir}", + enforceExistence: "index.ts", + files: [ + "index.ts", + "{pascal_tsx}", + "{css}", + "template.html", + "logo.svg", + "notes.weird", + ], + }, + }, + }, + }, + ], + walls: [], + }, +}; + +export const CASES = Object.freeze([ + { id: "clean", expectedViolation: false }, + { id: "placement", expectedViolation: true }, + { id: "required-sibling", expectedViolation: true }, + { id: "naming", expectedViolation: true }, + { id: "css", expectedViolation: true }, + { id: "html", expectedViolation: true }, + { id: "asset", expectedViolation: true }, + { id: "arbitrary-extension", expectedViolation: true }, + { id: "empty-directory", expectedViolation: true }, + { id: "ignored-directory", expectedViolation: false }, + { id: "generated-baseline", expectedViolation: false }, + { id: "permanent-exemption", expectedViolation: false }, +]); + +function addCase(root, caseId) { + switch (caseId) { + case "clean": + return; + case "placement": + write(root, "src/loose.ts", "export const loose = true;\n"); + return; + case "required-sibling": + write(root, "src/Missing/Missing.tsx", "export const Missing = () => null;\n"); + return; + case "naming": + write(root, "src/bad-name/index.ts", "export {};\n"); + return; + case "css": + write(root, "src/global.css", ":root { color: red; }\n"); + return; + case "html": + write(root, "src/index.html", "
fixture
\n"); + return; + case "asset": + write(root, "src/logo.svg", '\n'); + return; + case "arbitrary-extension": + write(root, "src/blob.unregistered", "fixture\n"); + return; + case "empty-directory": + mkdirSync(join(root, "src", "empty-directory"), { recursive: true }); + return; + case "ignored-directory": + write(root, "src/ignored/not-valid.ts", "export const ignored = true;\n"); + return; + case "generated-baseline": + write(root, "src/legacy.ts", "export const legacy = true;\n"); + return; + case "permanent-exemption": + write(root, "src/permanent.ts", "export const permanent = true;\n"); + return; + default: + throw new Error(`unknown fixture case: ${caseId}`); + } +} + +export function createFixture(root, { caseId = "clean", componentCount = 80 } = {}) { + write(root, "guard.config.json", `${JSON.stringify(CONFIG, null, 2)}\n`); + write( + root, + "eslint/baselines/topology.mjs", + `export const topologyStructureBaseline = ${ + caseId === "generated-baseline" ? '["legacy.ts"]' : "[]" + };\n`, + ); + write( + root, + "eslint/baselines/exempt.mjs", + `export const structureExempt = { topology: ${ + caseId === "permanent-exemption" ? '["permanent.ts"]' : "[]" + } };\n`, + ); + write(root, "src/index.ts", "export {};\n"); + + for (let index = 0; index < componentCount; index += 1) { + const name = `Component${String(index).padStart(3, "0")}`; + write(root, `src/${name}/index.ts`, "export {};\n"); + write(root, `src/${name}/${name}.tsx`, `export const ${name} = () => null;\n`); + write(root, `src/${name}/styles.css`, `.${name} {}\n`); + write(root, `src/${name}/template.html`, `
\n`); + write(root, `src/${name}/logo.svg`, '\n'); + write(root, `src/${name}/notes.weird`, "arbitrary extension fixture\n"); + } + + addCase(root, caseId); +} diff --git a/docs/benchmarks/experiments/2026-08-16-topology-guard/import-wall-current.mjs b/docs/benchmarks/experiments/2026-08-16-topology-guard/import-wall-current.mjs new file mode 100644 index 0000000..babe2cd --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-16-topology-guard/import-wall-current.mjs @@ -0,0 +1,50 @@ +import { createRequire } from "node:module"; +import { realpathSync } from "node:fs"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +export async function runImportWallFixture(cwd) { + cwd = realpathSync(cwd); + const require = createRequire(join(cwd, "package.json")); + const [{ ESLint }, { createIndependentModules, projectStructurePlugin }] = await Promise.all([ + import(pathToFileURL(require.resolve("eslint")).href), + import(pathToFileURL(require.resolve("eslint-plugin-project-structure")).href), + ]); + const eslint = new ESLint({ + cwd, + overrideConfigFile: true, + baseConfig: [ + { + files: ["src/**/*.js"], + plugins: { "project-structure": projectStructurePlugin }, + rules: { + "project-structure/independent-modules": [ + "error", + createIndependentModules({ + debugMode: true, + modules: [ + { + name: "feature-a", + pattern: "src/feature-a/**", + allowImportsFrom: ["src/feature-a/**"], + }, + { + name: "feature-b", + pattern: "src/feature-b/**", + allowImportsFrom: ["src/feature-b/**"], + }, + ], + }), + ], + }, + }, + ], + }); + const results = await eslint.lintFiles(["src"]); + const diagnostics = results.flatMap((result) => + result.messages + .filter((message) => message.ruleId === "project-structure/independent-modules") + .map((message) => ({ filePath: result.filePath, message: message.message })), + ); + return { code: diagnostics.length > 0 ? 1 : 0, diagnostics }; +} diff --git a/docs/benchmarks/experiments/2026-08-16-topology-guard/results.json b/docs/benchmarks/experiments/2026-08-16-topology-guard/results.json new file mode 100644 index 0000000..01434b2 --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-16-topology-guard/results.json @@ -0,0 +1,1001 @@ +{ + "schemaVersion": 1, + "recordedAt": "2026-08-16T00:08:09.036Z", + "sourceCommit": "4d624d059c8974aa58e0f3a27277cd6fd90383a4", + "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", + "node": "v24.19.0", + "logicalCpuCount": 10 + }, + "protocol": { + "warmups": 3, + "samples": 20, + "order": "alternating current-first/candidate-first", + "fixture": { + "componentCount": 80, + "files": 481, + "roots": 1 + }, + "current": "packed @norvalbv/devkit@0.51.1 installed inside the fixture; driver invokes its runStructureGate API", + "rss": "10ms sampling; sum RSS of /usr/bin/time wrapper and all descendants, floored by wait4 direct-child peak RSS", + "cpu": "/usr/bin/time -lp aggregate user + sys" + }, + "coverage": [ + { + "id": "clean", + "expectedViolation": false, + "current": { + "code": 0, + "errorCount": 0, + "text": "" + }, + "candidate": { + "code": 0, + "diagnostics": [] + } + }, + { + "id": "placement", + "expectedViolation": true, + "current": { + "code": 1, + "errorCount": 1, + "text": "\n/private/var/folders/9r/rmy4kywx6pl2hfzbw934txt40000gn/T/topology-benchmark-XSeBl4/src/loose.ts\n 0:1 error 🔥 File 'loose.ts' is invalid. 🔥\n\nAllowed names = index.ts\nError location = ./src/loose.ts\n\n project-structure/folder-structure\n\n✖ 1 problem (1 error, 0 warnings)\n\n🚫 Structure violations (folder-structure). Rename/relocate the file(s) to match the declared grammar, or (if intentional) re-grandfather via `devkit init`.\n" + }, + "candidate": { + "code": 1, + "diagnostics": [ + { + "tree": "topology", + "path": "src/loose.ts" + } + ] + } + }, + { + "id": "required-sibling", + "expectedViolation": true, + "current": { + "code": 1, + "errorCount": 1, + "text": "\n/private/var/folders/9r/rmy4kywx6pl2hfzbw934txt40000gn/T/topology-benchmark-XSeBl4/src/Missing/Missing.tsx\n 0:1 error 🔥 Folder 'Missing' enforces the existence of other folders/files. 🔥\n\nEnforce existence = ./src/Missing/index.ts\nError location = ./src/Missing\n\n project-structure/folder-structure\n\n✖ 1 problem (1 error, 0 warnings)\n\n🚫 Structure violations (folder-structure). Rename/relocate the file(s) to match the declared grammar, or (if intentional) re-grandfather via `devkit init`.\n" + }, + "candidate": { + "code": 1, + "diagnostics": [ + { + "tree": "topology", + "path": "src/Missing/Missing.tsx" + } + ] + } + }, + { + "id": "naming", + "expectedViolation": true, + "current": { + "code": 1, + "errorCount": 1, + "text": "\n/private/var/folders/9r/rmy4kywx6pl2hfzbw934txt40000gn/T/topology-benchmark-XSeBl4/src/bad-name/index.ts\n 0:1 error 🔥 Folder 'bad-name' is invalid. 🔥\n\nAllowed names = {pascal_dir}\nError location = ./src/bad-name\n\n project-structure/folder-structure\n\n✖ 1 problem (1 error, 0 warnings)\n\n🚫 Structure violations (folder-structure). Rename/relocate the file(s) to match the declared grammar, or (if intentional) re-grandfather via `devkit init`.\n" + }, + "candidate": { + "code": 1, + "diagnostics": [ + { + "tree": "topology", + "path": "src/bad-name/index.ts" + } + ] + } + }, + { + "id": "css", + "expectedViolation": true, + "current": { + "code": 0, + "errorCount": 0, + "text": "" + }, + "candidate": { + "code": 1, + "diagnostics": [ + { + "tree": "topology", + "path": "src/global.css" + } + ] + } + }, + { + "id": "html", + "expectedViolation": true, + "current": { + "code": 0, + "errorCount": 0, + "text": "" + }, + "candidate": { + "code": 1, + "diagnostics": [ + { + "tree": "topology", + "path": "src/index.html" + } + ] + } + }, + { + "id": "asset", + "expectedViolation": true, + "current": { + "code": 0, + "errorCount": 0, + "text": "" + }, + "candidate": { + "code": 1, + "diagnostics": [ + { + "tree": "topology", + "path": "src/logo.svg" + } + ] + } + }, + { + "id": "arbitrary-extension", + "expectedViolation": true, + "current": { + "code": 0, + "errorCount": 0, + "text": "" + }, + "candidate": { + "code": 1, + "diagnostics": [ + { + "tree": "topology", + "path": "src/blob.unregistered" + } + ] + } + }, + { + "id": "empty-directory", + "expectedViolation": true, + "current": { + "code": 0, + "errorCount": 0, + "text": "" + }, + "candidate": { + "code": 0, + "diagnostics": [] + } + }, + { + "id": "ignored-directory", + "expectedViolation": false, + "current": { + "code": 0, + "errorCount": 0, + "text": "" + }, + "candidate": { + "code": 0, + "diagnostics": [] + } + }, + { + "id": "generated-baseline", + "expectedViolation": false, + "current": { + "code": 0, + "errorCount": 0, + "text": "" + }, + "candidate": { + "code": 0, + "diagnostics": [] + } + }, + { + "id": "permanent-exemption", + "expectedViolation": false, + "current": { + "code": 0, + "errorCount": 0, + "text": "" + }, + "candidate": { + "code": 0, + "diagnostics": [] + } + }, + { + "id": "import-wall", + "expectedViolation": true, + "currentBroaderEslintOwner": { + "code": 1, + "diagnostics": [ + { + "filePath": "/private/var/folders/9r/rmy4kywx6pl2hfzbw934txt40000gn/T/topology-benchmark-XSeBl4/src/feature-a/index.js", + "message": "🔥 This import is not allowed in the module 'feature-a'. 🔥\n\nFile path = \"src/feature-a/index.js\"\nImport path = \"src/feature-b/internal.js\"\n{family} = \"NO_FAMILY\"\n{dirname} = \"src/feature-a\"\n\n\"\\\"allowImportsFrom:\\\\n\\\"\\\"src/feature-a/**\\\"\\n\"\n" + } + ] + }, + "candidate": { + "code": 0, + "diagnostics": [] + } + } + ], + "lanes": { + "clean": { + "fixtureCase": "clean", + "samples": { + "current": [ + { + "wallMs": 249.0287089999997, + "cpuMs": 470, + "userMs": 210, + "systemMs": 260, + "treePeakRssBytes": 120602624, + "directPeakRssBytes": 120602624 + }, + { + "wallMs": 258.9182499999997, + "cpuMs": 500, + "userMs": 220, + "systemMs": 280, + "treePeakRssBytes": 121978880, + "directPeakRssBytes": 120782848 + }, + { + "wallMs": 253.942583, + "cpuMs": 500, + "userMs": 220, + "systemMs": 280, + "treePeakRssBytes": 120520704, + "directPeakRssBytes": 120520704 + }, + { + "wallMs": 246.35812499999975, + "cpuMs": 480, + "userMs": 210, + "systemMs": 270, + "treePeakRssBytes": 121864192, + "directPeakRssBytes": 121864192 + }, + { + "wallMs": 251.11295900000005, + "cpuMs": 480, + "userMs": 210, + "systemMs": 270, + "treePeakRssBytes": 122175488, + "directPeakRssBytes": 121094144 + }, + { + "wallMs": 252.43329100000028, + "cpuMs": 490, + "userMs": 220, + "systemMs": 270, + "treePeakRssBytes": 121995264, + "directPeakRssBytes": 120946688 + }, + { + "wallMs": 247.55554200000006, + "cpuMs": 470, + "userMs": 210, + "systemMs": 260, + "treePeakRssBytes": 121798656, + "directPeakRssBytes": 121798656 + }, + { + "wallMs": 246.24666699999943, + "cpuMs": 480, + "userMs": 210, + "systemMs": 270, + "treePeakRssBytes": 121946112, + "directPeakRssBytes": 121028608 + }, + { + "wallMs": 244.8822910000008, + "cpuMs": 480, + "userMs": 210, + "systemMs": 270, + "treePeakRssBytes": 121765888, + "directPeakRssBytes": 120848384 + }, + { + "wallMs": 247.62233300000025, + "cpuMs": 480, + "userMs": 210, + "systemMs": 270, + "treePeakRssBytes": 121028608, + "directPeakRssBytes": 121028608 + }, + { + "wallMs": 258.0616250000003, + "cpuMs": 490, + "userMs": 210, + "systemMs": 280, + "treePeakRssBytes": 121241600, + "directPeakRssBytes": 121241600 + }, + { + "wallMs": 251.89137500000015, + "cpuMs": 470, + "userMs": 210, + "systemMs": 260, + "treePeakRssBytes": 121208832, + "directPeakRssBytes": 121208832 + }, + { + "wallMs": 255.6784579999994, + "cpuMs": 480, + "userMs": 220, + "systemMs": 260, + "treePeakRssBytes": 122257408, + "directPeakRssBytes": 121208832 + }, + { + "wallMs": 262.6339170000001, + "cpuMs": 490, + "userMs": 220, + "systemMs": 270, + "treePeakRssBytes": 121880576, + "directPeakRssBytes": 121880576 + }, + { + "wallMs": 255.48300000000017, + "cpuMs": 490, + "userMs": 220, + "systemMs": 270, + "treePeakRssBytes": 122454016, + "directPeakRssBytes": 121585664 + }, + { + "wallMs": 261.84049999999934, + "cpuMs": 490, + "userMs": 220, + "systemMs": 270, + "treePeakRssBytes": 120782848, + "directPeakRssBytes": 120782848 + }, + { + "wallMs": 297.2985840000001, + "cpuMs": 550, + "userMs": 250, + "systemMs": 300, + "treePeakRssBytes": 120848384, + "directPeakRssBytes": 120848384 + }, + { + "wallMs": 246.64466600000014, + "cpuMs": 490, + "userMs": 220, + "systemMs": 270, + "treePeakRssBytes": 121520128, + "directPeakRssBytes": 121520128 + }, + { + "wallMs": 275.1935830000002, + "cpuMs": 520, + "userMs": 230, + "systemMs": 290, + "treePeakRssBytes": 121372672, + "directPeakRssBytes": 121372672 + }, + { + "wallMs": 282.64387499999975, + "cpuMs": 520, + "userMs": 240, + "systemMs": 280, + "treePeakRssBytes": 121716736, + "directPeakRssBytes": 121716736 + } + ], + "candidate": [ + { + "wallMs": 91.50637499999993, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83148800, + "directPeakRssBytes": 83148800 + }, + { + "wallMs": 90.35912499999995, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83132416, + "directPeakRssBytes": 83132416 + }, + { + "wallMs": 88.08775000000014, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 82771968, + "directPeakRssBytes": 82771968 + }, + { + "wallMs": 88.66691700000001, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83394560, + "directPeakRssBytes": 83394560 + }, + { + "wallMs": 89.04191700000001, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83673088, + "directPeakRssBytes": 83673088 + }, + { + "wallMs": 90.60083299999997, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83263488, + "directPeakRssBytes": 83263488 + }, + { + "wallMs": 89.29266699999971, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83034112, + "directPeakRssBytes": 83034112 + }, + { + "wallMs": 86.65466599999945, + "cpuMs": 130, + "userMs": 60, + "systemMs": 70, + "treePeakRssBytes": 82755584, + "directPeakRssBytes": 82755584 + }, + { + "wallMs": 87.61720800000057, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 82837504, + "directPeakRssBytes": 82837504 + }, + { + "wallMs": 88.33441700000003, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 82608128, + "directPeakRssBytes": 82608128 + }, + { + "wallMs": 97.90608400000019, + "cpuMs": 130, + "userMs": 60, + "systemMs": 70, + "treePeakRssBytes": 83001344, + "directPeakRssBytes": 83001344 + }, + { + "wallMs": 89.542958, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 82984960, + "directPeakRssBytes": 82984960 + }, + { + "wallMs": 89.48066700000072, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83296256, + "directPeakRssBytes": 83296256 + }, + { + "wallMs": 106.22491700000046, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83263488, + "directPeakRssBytes": 83263488 + }, + { + "wallMs": 98.57729199999994, + "cpuMs": 140, + "userMs": 70, + "systemMs": 70, + "treePeakRssBytes": 83312640, + "directPeakRssBytes": 83312640 + }, + { + "wallMs": 98.73154199999954, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 82837504, + "directPeakRssBytes": 82837504 + }, + { + "wallMs": 102.19804099999965, + "cpuMs": 130, + "userMs": 60, + "systemMs": 70, + "treePeakRssBytes": 82837504, + "directPeakRssBytes": 82837504 + }, + { + "wallMs": 104.89750000000004, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 82952192, + "directPeakRssBytes": 82542592 + }, + { + "wallMs": 108.96904200000063, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83427328, + "directPeakRssBytes": 83427328 + }, + { + "wallMs": 91.57516699999906, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83230720, + "directPeakRssBytes": 83230720 + } + ] + }, + "summary": { + "current": { + "wallMs": { + "median": 253.18793700000015, + "p95": 282.64387499999975 + }, + "cpuMs": { + "median": 490, + "p95": 520 + }, + "userMs": { + "median": 220, + "p95": 240 + }, + "systemMs": { + "median": 270, + "p95": 290 + }, + "treePeakRssBytes": { + "median": 121741312, + "p95": 122257408 + }, + "directPeakRssBytes": { + "median": 121151488, + "p95": 121864192 + } + }, + "candidate": { + "wallMs": { + "median": 90.47997899999996, + "p95": 106.22491700000046 + }, + "cpuMs": { + "median": 120, + "p95": 130 + }, + "userMs": { + "median": 60, + "p95": 60 + }, + "systemMs": { + "median": 60, + "p95": 70 + }, + "treePeakRssBytes": { + "median": 83083264, + "p95": 83427328 + }, + "directPeakRssBytes": { + "median": 83083264, + "p95": 83427328 + } + } + } + }, + "placement": { + "fixtureCase": "placement", + "samples": { + "current": [ + { + "wallMs": 268.46099999999933, + "cpuMs": 510, + "userMs": 230, + "systemMs": 280, + "treePeakRssBytes": 122257408, + "directPeakRssBytes": 122257408 + }, + { + "wallMs": 276.23741599999994, + "cpuMs": 520, + "userMs": 240, + "systemMs": 280, + "treePeakRssBytes": 122372096, + "directPeakRssBytes": 121487360 + }, + { + "wallMs": 301.3277500000004, + "cpuMs": 560, + "userMs": 260, + "systemMs": 300, + "treePeakRssBytes": 121749504, + "directPeakRssBytes": 121749504 + }, + { + "wallMs": 253.26499999999942, + "cpuMs": 500, + "userMs": 230, + "systemMs": 270, + "treePeakRssBytes": 120471552, + "directPeakRssBytes": 120471552 + }, + { + "wallMs": 261.3679580000007, + "cpuMs": 490, + "userMs": 220, + "systemMs": 270, + "treePeakRssBytes": 120832000, + "directPeakRssBytes": 120832000 + }, + { + "wallMs": 259.5215840000001, + "cpuMs": 500, + "userMs": 230, + "systemMs": 270, + "treePeakRssBytes": 122208256, + "directPeakRssBytes": 121372672 + }, + { + "wallMs": 254.39695799999936, + "cpuMs": 500, + "userMs": 230, + "systemMs": 270, + "treePeakRssBytes": 121339904, + "directPeakRssBytes": 121339904 + }, + { + "wallMs": 270.95970900000066, + "cpuMs": 500, + "userMs": 230, + "systemMs": 270, + "treePeakRssBytes": 121602048, + "directPeakRssBytes": 121602048 + }, + { + "wallMs": 261.33541700000023, + "cpuMs": 500, + "userMs": 230, + "systemMs": 270, + "treePeakRssBytes": 122339328, + "directPeakRssBytes": 122339328 + }, + { + "wallMs": 274.3782080000001, + "cpuMs": 500, + "userMs": 230, + "systemMs": 270, + "treePeakRssBytes": 120913920, + "directPeakRssBytes": 120913920 + }, + { + "wallMs": 275.2072499999995, + "cpuMs": 530, + "userMs": 240, + "systemMs": 290, + "treePeakRssBytes": 122208256, + "directPeakRssBytes": 121339904 + }, + { + "wallMs": 277.34375, + "cpuMs": 520, + "userMs": 240, + "systemMs": 280, + "treePeakRssBytes": 120815616, + "directPeakRssBytes": 120815616 + }, + { + "wallMs": 266.9014169999991, + "cpuMs": 500, + "userMs": 230, + "systemMs": 270, + "treePeakRssBytes": 121159680, + "directPeakRssBytes": 121159680 + }, + { + "wallMs": 262.7869159999991, + "cpuMs": 500, + "userMs": 230, + "systemMs": 270, + "treePeakRssBytes": 121651200, + "directPeakRssBytes": 120766464 + }, + { + "wallMs": 260.56333300000006, + "cpuMs": 490, + "userMs": 220, + "systemMs": 270, + "treePeakRssBytes": 120832000, + "directPeakRssBytes": 120832000 + }, + { + "wallMs": 274.5217499999999, + "cpuMs": 520, + "userMs": 240, + "systemMs": 280, + "treePeakRssBytes": 122306560, + "directPeakRssBytes": 121290752 + }, + { + "wallMs": 265.87191600000006, + "cpuMs": 500, + "userMs": 230, + "systemMs": 270, + "treePeakRssBytes": 121896960, + "directPeakRssBytes": 120700928 + }, + { + "wallMs": 258.45195899999817, + "cpuMs": 490, + "userMs": 230, + "systemMs": 260, + "treePeakRssBytes": 122290176, + "directPeakRssBytes": 121356288 + }, + { + "wallMs": 259.55433300000004, + "cpuMs": 480, + "userMs": 220, + "systemMs": 260, + "treePeakRssBytes": 122290176, + "directPeakRssBytes": 122290176 + }, + { + "wallMs": 256.5051249999997, + "cpuMs": 470, + "userMs": 210, + "systemMs": 260, + "treePeakRssBytes": 120455168, + "directPeakRssBytes": 120455168 + } + ], + "candidate": [ + { + "wallMs": 88.25637500000084, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83394560, + "directPeakRssBytes": 83394560 + }, + { + "wallMs": 87.69933300000048, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83181568, + "directPeakRssBytes": 83181568 + }, + { + "wallMs": 86.56704100000024, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83132416, + "directPeakRssBytes": 83132416 + }, + { + "wallMs": 97.01929199999904, + "cpuMs": 130, + "userMs": 60, + "systemMs": 70, + "treePeakRssBytes": 82640896, + "directPeakRssBytes": 82640896 + }, + { + "wallMs": 90.05395900000076, + "cpuMs": 130, + "userMs": 60, + "systemMs": 70, + "treePeakRssBytes": 83017728, + "directPeakRssBytes": 83017728 + }, + { + "wallMs": 88.84591699999874, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83132416, + "directPeakRssBytes": 83132416 + }, + { + "wallMs": 87.48979099999997, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83132416, + "directPeakRssBytes": 83132416 + }, + { + "wallMs": 104.91554199999882, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83296256, + "directPeakRssBytes": 83296256 + }, + { + "wallMs": 86.92720799999915, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 82853888, + "directPeakRssBytes": 82853888 + }, + { + "wallMs": 88.91833299999962, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83312640, + "directPeakRssBytes": 83312640 + }, + { + "wallMs": 85.6432079999995, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83034112, + "directPeakRssBytes": 83034112 + }, + { + "wallMs": 87.8887080000004, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 82935808, + "directPeakRssBytes": 82935808 + }, + { + "wallMs": 87.09137499999997, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83247104, + "directPeakRssBytes": 83247104 + }, + { + "wallMs": 97.82208399999945, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 82739200, + "directPeakRssBytes": 82739200 + }, + { + "wallMs": 87.71291700000074, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83656704, + "directPeakRssBytes": 83656704 + }, + { + "wallMs": 85.14908300000025, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83132416, + "directPeakRssBytes": 83132416 + }, + { + "wallMs": 84.33829199999855, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83410944, + "directPeakRssBytes": 83410944 + }, + { + "wallMs": 87.08299999999872, + "cpuMs": 110, + "userMs": 50, + "systemMs": 60, + "treePeakRssBytes": 83394560, + "directPeakRssBytes": 83394560 + }, + { + "wallMs": 84.24537500000224, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 83066880, + "directPeakRssBytes": 83066880 + }, + { + "wallMs": 85.46637499999997, + "cpuMs": 120, + "userMs": 60, + "systemMs": 60, + "treePeakRssBytes": 82722816, + "directPeakRssBytes": 82722816 + } + ] + }, + "summary": { + "current": { + "wallMs": { + "median": 264.3294159999996, + "p95": 277.34375 + }, + "cpuMs": { + "median": 500, + "p95": 530 + }, + "userMs": { + "median": 230, + "p95": 240 + }, + "systemMs": { + "median": 270, + "p95": 290 + }, + "treePeakRssBytes": { + "median": 121700352, + "p95": 122339328 + }, + "directPeakRssBytes": { + "median": 121315328, + "p95": 122290176 + } + }, + "candidate": { + "wallMs": { + "median": 87.59456200000022, + "p95": 97.82208399999945 + }, + "cpuMs": { + "median": 120, + "p95": 130 + }, + "userMs": { + "median": 60, + "p95": 60 + }, + "systemMs": { + "median": 60, + "p95": 70 + }, + "treePeakRssBytes": { + "median": 83132416, + "p95": 83410944 + }, + "directPeakRssBytes": { + "median": 83132416, + "p95": 83410944 + } + } + } + } + } +} diff --git a/docs/decisions/oxc-toolchain-migration.md b/docs/decisions/oxc-toolchain-migration.md index 8ef1028..7096f61 100644 --- a/docs/decisions/oxc-toolchain-migration.md +++ b/docs/decisions/oxc-toolchain-migration.md @@ -21,3 +21,4 @@ created: 2026-08-15 **Source:** shortcut · sc-1674 - 2026-08-15 — sc-1675 packages exact oxlint@1.78.0 and oxfmt@0.63.0 pins behind an opt-in package/standalone capability. devkit oxc resolves those binaries through Devkit's own module graph; init/upgrade/doctor/clean manage a replaceable Oxlint base plus collision-safe consumer-owned root configs. Existing Biome, ESLint topology, tsc, scripts, and hooks remain unchanged until their separate parity tickets satisfy this Target's evidence gates; overlay and self-host activation remain unsupported. - 2026-08-16 — sc-1677 retains Biome 2.5.6 as Devkit's ordinary JavaScript, TypeScript, JSON, and CSS linter. On the same 535-file JS/TS manifest under supported Node 24.19.0, the pinned native Oxlint candidate used 89.1% less median CPU and 35.0% less median process-tree RSS, but it produced 18 findings against Biome's clean 0, missed the explicitly enabled useTopLevelRegex fixture, and selected no file for JSON duplicate-key or CSS unknown-property fixtures. Revisit after Devkit freezes an explicit desired-rule manifest and provides parity owners for JSON, CSS, regex allocation, and the React preset; do not claim the speed-up by silently adopting a smaller rule surface. +- 2026-08-16 — sc-1678 measured the direct filesystem walker at 64.3% lower median clean-tree wall time, 75.5% lower CPU, and 31.8% lower process-tree peak RSS than the packaged guard-structure ESLint chain on a 481-file fixture under Node 24.19 (placement-violation lane: 66.9%/76.0%/31.7% lower). Retain ESLint: the prototype has no independent-modules import-wall parser/resolver, drops directory-only violations such as an illegal empty directory, and cannot ingest Electron's six hand-written folder trees because its guard config has no structure representation. It therefore fails the Target's coverage-parity condition despite the material speed win. Evidence: docs/benchmarks/experiments/2026-08-16-topology-guard/.