From 301cbcb653713f40297a5e39a269534e675e9cde Mon Sep 17 00:00:00 2001 From: norvalbv Date: Sun, 16 Aug 2026 01:31:22 +0100 Subject: [PATCH] feat(format): adopt oxfmt for devkit self-host (sc-1679) ## Summary - adopt pinned Oxfmt 0.63.0 for Devkit's exact root formatting scope and CI format verdict - use the direct pinned Oxfmt binary in Devkit's self-host staged hook, with one thread and an exact authored-path allowlist - keep Biome 2.5.6 as the lint/assist owner and retain Biome in every generic package/standalone consumer hook - commit the seven reviewed one-time formatter changes and the reproducible decision evidence ## Ownership boundary This is intentionally a Devkit-only migration. `format` and `format:check` now select the same authored surface proven in the experiment; CI runs `format:check` before formatter-disabled Biome lint. The self-host hook mirrors that allowlist, includes `.mts`, fails closed on Oxfmt errors, and never selects benchmark evidence, vendored sources, generated output, or generic consumer files. Direct execution tests prove that: - an in-scope staged `.mts` file is formatted and re-staged - staged benchmark evidence remains byte-identical - a partially staged file is preserved according to the index/worktree state observed at hook start - a failing Oxfmt process blocks the hook - source, generated dist, doctor output, and committed-hook parity agree Concurrent writers to the same worktree remain unsupported; agents must use isolated worktrees. ## Output parity Biome reported 558 files and the shadow Oxfmt invocation selected the same 558 files. The committed `.oxfmtrc.json` adds itself as the 559th managed file after adoption. Seven TypeScript files changed: six whitespace/layout-only; one expanded test call also gained the formatter's permitted trailing comma. Identifiers, strings, operators, imports, comments, and control flow are unchanged. A second full write was byte-idempotent. ## Benchmark | Full-scope runner | Median wall | Median CPU | Median tree RSS | | --- | ---: | ---: | ---: | | Biome direct | 0.1997 s | 0.8709 s | 143.7 MiB | | Oxfmt direct, default threads | 0.1242 s | 0.2382 s | 109.1 MiB | | Delta | -37.8% | -72.6% | -24.1% | The one-file check-mode proxy measured direct Oxfmt at 8.1% less CPU but higher wall/RSS than Biome. It is recorded as startup characterization, not an exact write-mode hook speedup. The same proxy shows the portable `devkit oxc fmt` wrapper adds 211.8% CPU and 245.3% RSS versus Biome, so the hot self-host path uses the pinned binary directly. Method: Node 24.19.0, 3 discarded warm-ups, 10 alternating paired samples, monotonic wall clock, wait4 CPU, and complete process-tree RSS sampling. ## Validation - `bun run format:check` - `bun run lint` - `bun run typecheck` - `bun run lint:structure` - `bun run build` - 190 focused self-host, apply-init, hook-block, drift, doctor, and dist tests passed - full suite: 3,820 passed / 5 skipped; one unchanged parallel timing assertion failed under load and passed isolated - benchmark and decision checks - second-pass formatter idempotence - GitNexus staged analysis: HIGH because the self-host/doctor seam and seven indexed functions are touched; all affected flows were impact-reviewed and covered by focused tests Shortcut: sc-1679 --- .github/workflows/gate.yml | 5 +- .husky/pre-commit | 6 +- .oxfmtrc.json | 24 ++++ README.md | 1 + cli/__tests__/self-host.test.mts | 86 ++++++++++- cli/lib/husky/self-host.mts | 36 ++++- dist/README.md | 1 + dist/cli/lib/husky/self-host.mjs | 32 ++++- dist/package.json | 5 +- .../2026-08-16-oxfmt-devkit/README.md | 133 ++++++++++++++++++ docs/decisions/oxc-toolchain-migration.md | 1 + gate-engine/critique/capture-normalizer.mts | 9 +- .../decisions/__tests__/eval-bench.test.mts | 40 +++--- gate-engine/decisions/decision-format.mts | 8 +- gate-engine/eval/render.mts | 8 +- gate-engine/review/baseline-gate.mts | 2 +- gate-engine/review/run-review.mts | 28 ++-- gate-engine/structure/walk.mts | 4 +- package.json | 5 +- 19 files changed, 362 insertions(+), 72 deletions(-) create mode 100644 .oxfmtrc.json create mode 100644 docs/benchmarks/experiments/2026-08-16-oxfmt-devkit/README.md diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 72589bd9..d5f37ec0 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -23,7 +23,10 @@ jobs: - name: Install run: bun install --frozen-lockfile - - name: Lint (Biome) + - name: Format (Oxfmt) + run: bun run format:check + + - name: Lint and assist (Biome) run: bun run lint - name: Structure governance (folder-structure) diff --git a/.husky/pre-commit b/.husky/pre-commit index a4d9a235..7804a578 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -99,10 +99,10 @@ __dk_review_baseline_gate() { } # devkit:biome-format -# Format staged files with biome, then re-stage exactly those (scoped โ€” never a blanket +# Format staged files with Oxfmt, then re-stage exactly those (scoped โ€” never a blanket # `git add -u`, which would sweep unrelated working-tree changes into the commit). Only # re-add files with NO unstaged edits, so partially-staged files commit exactly as staged. -STAGED_FMT=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(tsx?|jsx?|css|json|jsonc|mjs)$' || true) +STAGED_FMT=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^((cli|gate-engine)/.*\.(tsx?|jsx?|css|jsonc?|mjs|mts)|(tsconfig|biome)/.*\.jsonc?|skills/.*\.mjs|\.co-occurrence-allowlist\.json|\.fallowrc\.jsonc|\.oxfmtrc\.json|biome\.jsonc|eslint\.config\.mjs|guard\.config(\.example)?\.json|package\.json|search-code\.config\.json|tsconfig(\.build)?\.json|vitest(\.e2e)?\.config\.mjs|vitest\.setup\.mjs)$' || true) if [ -n "$STAGED_FMT" ]; then UNSTAGED_FMT_FILE=$(mktemp) git diff --name-only | sort -u >"$UNSTAGED_FMT_FILE" @@ -110,7 +110,7 @@ if [ -n "$STAGED_FMT" ]; then rm -f "$UNSTAGED_FMT_FILE" if [ -n "$FMT_SAFE" ]; then echo "๐ŸŽจ Formatting staged files..." - echo "$FMT_SAFE" | xargs bunx biome format --write 2>/dev/null || true + echo "$FMT_SAFE" | xargs node_modules/.bin/oxfmt --threads 1 --write || exit 1 echo "$FMT_SAFE" | xargs git add -f fi fi diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000..fb3fe8ae --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,24 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "useTabs": false, + "tabWidth": 2, + "printWidth": 100, + "singleQuote": true, + "jsxSingleQuote": false, + "quoteProps": "as-needed", + "trailingComma": "all", + "semi": true, + "arrowParens": "always", + "bracketSameLine": false, + "bracketSpacing": true, + "sortPackageJson": false, + "overrides": [ + { + "files": ["**/*.json", "**/*.jsonc"], + "options": { + "trailingComma": "none" + } + } + ], + "ignorePatterns": ["node_modules", "templates", "bun.lock", "dist"] +} diff --git a/README.md b/README.md index e9aae1d4..4a6dd039 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ bun install --frozen-lockfile bun run devkit -- bun run test:run bun run typecheck +bun run format:check bun run lint bun run lint:structure bun run benchmarks:check diff --git a/cli/__tests__/self-host.test.mts b/cli/__tests__/self-host.test.mts index 3f51e7dc..464b2412 100644 --- a/cli/__tests__/self-host.test.mts +++ b/cli/__tests__/self-host.test.mts @@ -7,16 +7,18 @@ import { chmodSync, existsSync, + mkdirSync, mkdtempSync, readdirSync, readFileSync, + symlinkSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import { extractGuardBlock, replaceGuardBlock } from '../lib/husky/husky-block.mts'; +import { buildFullHook, extractGuardBlock, replaceGuardBlock } from '../lib/husky/husky-block.mts'; import { DK_NO_GIT_ENV_HELPER } from '../lib/husky/review-fragments.mts'; import { buildSelfHostBlock, @@ -51,15 +53,22 @@ describe('self-host bin rewrite', () => { expect(() => sourceBinFor(ROOT, 'guard-nope')).toThrow(/no bin/); }); - it('toSelfHost rewrites `bunx guard-*` to `node ` and leaves `bunx biome` alone', () => { + it('toSelfHost rewrites source gates and the self-host formatter without changing consumers', () => { const input = 'bunx guard-review --gate\nbunx biome format --write\nbunx guard-deterministic --hook x'; const out = toSelfHost(input, ROOT); expect(out).toContain('node gate-engine/review/cli.mts --gate'); expect(out).toContain('node gate-engine/deterministic/run.mts --hook x'); - expect(out).toContain('bunx biome format --write'); // real devDep โ€” untouched + expect(out).toContain('node_modules/.bin/oxfmt --threads 1 --write'); + expect(out).not.toContain('bunx biome format --write'); expect(out).not.toContain('bunx guard-'); }); + + it('leaves the generic consumer hook on Biome until that repository proves parity', () => { + const hook = buildFullHook({ biome: true, guards: [] }); + expect(hook).toContain('bunx biome format --write'); + expect(hook).not.toContain('node_modules/.bin/oxfmt'); + }); }); describe('selfHostSelection', () => { @@ -111,10 +120,79 @@ describe('buildSelfHostHook', () => { expect(hook).toContain('--extra "lint=bun run lint"'); expect(hook).toContain('--extra "benchmarks=bun run benchmarks:check -- --mode staged"'); expect(hook).toContain('--structure "bun run lint:structure"'); + expect(hook).toContain('node_modules/.bin/oxfmt --threads 1 --write'); + expect(hook).toContain('(cli|gate-engine)/'); + expect(hook).toContain('skills/.*\\.mjs'); + expect(hook).toContain('node_modules/.bin/oxfmt --threads 1 --write || exit 1'); + expect(hook).not.toContain('oxfmt --threads 1 --write 2>/dev/null || true'); + expect(hook).not.toContain("grep -E '\\.(tsx?|jsx?|css|json|jsonc|mjs|mts)$'"); + expect(hook).not.toContain('bunx biome format --write'); expect(hook).not.toMatch(/bunx guard-/); expect(hook).not.toContain('@norvalbv/devkit'); }); + it('formats and re-stages only files inside the proven self-host scope', () => { + const root = mkdtempSync(join(tmpdir(), 'self-host-oxfmt-')); + execFileSync('git', ['init', '-q'], { cwd: root }); + symlinkSync(join(ROOT, 'node_modules'), join(root, 'node_modules'), 'dir'); + mkdirSync(join(root, 'cli'), { recursive: true }); + mkdirSync(join(root, 'docs', 'benchmarks'), { recursive: true }); + writeFileSync(join(root, '.oxfmtrc.json'), '{}\n'); + writeFileSync(join(root, 'cli', 'sample.mts'), 'const value={answer:42}\n'); + writeFileSync(join(root, 'cli', 'partial.mts'), 'const partial={staged:true}\n'); + writeFileSync(join(root, 'docs', 'benchmarks', 'catalog.json'), '{"evidence":true}\n'); + execFileSync( + 'git', + ['add', '.oxfmtrc.json', 'cli/sample.mts', 'cli/partial.mts', 'docs/benchmarks/catalog.json'], + { cwd: root }, + ); + writeFileSync(join(root, 'cli', 'partial.mts'), 'const partial={working:true}\n'); + + const fragment = buildSelfHostHook(HOOK_SEL, '', ROOT).match( + /# devkit:biome-format[\s\S]*?# \/devkit:biome-format/, + )?.[0]; + expect(fragment).toBeDefined(); + execFileSync('sh', ['-c', fragment ?? 'exit 1'], { cwd: root }); + + const formatted = 'const value = { answer: 42 };\n'; + expect(readFileSync(join(root, 'cli', 'sample.mts'), 'utf8')).toBe(formatted); + expect(execFileSync('git', ['show', ':cli/sample.mts'], { cwd: root, encoding: 'utf8' })).toBe( + formatted, + ); + const evidence = '{"evidence":true}\n'; + expect(readFileSync(join(root, 'docs', 'benchmarks', 'catalog.json'), 'utf8')).toBe(evidence); + expect( + execFileSync('git', ['show', ':docs/benchmarks/catalog.json'], { + cwd: root, + encoding: 'utf8', + }), + ).toBe(evidence); + expect(readFileSync(join(root, 'cli', 'partial.mts'), 'utf8')).toBe( + 'const partial={working:true}\n', + ); + expect(execFileSync('git', ['show', ':cli/partial.mts'], { cwd: root, encoding: 'utf8' })).toBe( + 'const partial={staged:true}\n', + ); + }); + + it('blocks the self-host hook when Oxfmt fails', () => { + const root = mkdtempSync(join(tmpdir(), 'self-host-oxfmt-failure-')); + execFileSync('git', ['init', '-q'], { cwd: root }); + mkdirSync(join(root, 'cli'), { recursive: true }); + mkdirSync(join(root, 'node_modules', '.bin'), { recursive: true }); + const oxfmt = join(root, 'node_modules', '.bin', 'oxfmt'); + writeFileSync(oxfmt, '#!/bin/sh\nexit 7\n'); + chmodSync(oxfmt, 0o755); + writeFileSync(join(root, 'cli', 'sample.mts'), 'const value={answer:42}\n'); + execFileSync('git', ['add', 'cli/sample.mts'], { cwd: root }); + + const fragment = buildSelfHostHook(HOOK_SEL, '', ROOT).match( + /# devkit:biome-format[\s\S]*?# \/devkit:biome-format/, + )?.[0]; + expect(fragment).toBeDefined(); + 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 @@ -126,7 +204,7 @@ describe('buildSelfHostHook', () => { readFileSync(join(ROOT, 'package.json'), 'utf8'), ); expect(SELF_HOST_EXTRAS).toContainEqual({ label: 'lint', cmd: 'bun run lint' }); - expect(pkg.scripts?.lint).toContain('--error-on-warnings'); + expect(pkg.scripts?.lint).toBe('biome check --formatter-enabled=false --error-on-warnings .'); }); it('preserves the advisory fallow-audit gate INSIDE the block (never blocks, survives re-run)', () => { diff --git a/cli/lib/husky/self-host.mts b/cli/lib/husky/self-host.mts index e737bd00..1d06afac 100644 --- a/cli/lib/husky/self-host.mts +++ b/cli/lib/husky/self-host.mts @@ -32,11 +32,13 @@ import { } from './husky-block.mts'; // devkit's own structure-lint command (package.json `lint:structure` = `eslint cli gate-engine`) -// and its hard biome-lint gate (`lint` = `biome check --error-on-warnings .` โ€” biome exits 0 when +// and its hard Biome lint/assist gate (`lint` disables Biome's formatter explicitly โ€” Biome exits 0 when // every diagnostic is warn-severity, so without that flag the gate PRINTS its findings into the log // and passes the commit anyway). The hard-lint is folded into the deterministic orchestrator via // `--extra` (any non-zero blocks); both run via real devDeps -// (eslint/biome), so toSelfHost leaves them untouched. Together with the advisory fallow fragment +// (eslint/biome). Formatting is the one self-host-only rewrite beyond guard bins: Devkit has proven +// its pinned Oxfmt output over its own authored scope, while consumer hooks stay on Biome until each +// consumer completes the same parity exercise. Together with the advisory fallow fragment // below, the self-host hook preserves every gate the pre-self-host hand hook ran AND adds review + dup/clone. export const SELF_HOST_STRUCTURE_CMD = 'bun run lint:structure'; export const SELF_HOST_EXTRAS: Array<{ label: string; cmd: string }> = [ @@ -74,8 +76,21 @@ type SelfHostHookInput = Selection & { }; // Matches the `bunx guard-` bins the generator emits. `guard-qavis-advisory` (double hyphen) is -// covered by `[a-z-]+`. `bunx biome` has no `guard-` prefix โ†’ correctly left alone. +// covered by `[a-z-]+`. The formatter has its own exact rewrite below so consumer output is not +// affected by the self-host-only Oxfmt adoption. const BUNX_GUARD_RE = /\bbunx (guard-[a-z-]+)\b/g; +const BUNX_BIOME_FORMAT_RE = /\bbunx biome format --write\b/g; +const BIOME_FORMAT_COMMENT_RE = /Format staged files with biome/g; +const BIOME_FORMAT_EXTENSIONS = '\\.(tsx?|jsx?|css|json|jsonc|mjs)$'; +const BIOME_FORMAT_FILTER = `grep -E '${BIOME_FORMAT_EXTENSIONS}'`; +const SELF_HOST_OXFMT_BEST_EFFORT = + 'node_modules/.bin/oxfmt --threads 1 --write 2>/dev/null || true'; +const SELF_HOST_OXFMT_HARD = 'node_modules/.bin/oxfmt --threads 1 --write || exit 1'; +// Keep the staged hook on the same authored-file boundary as package.json's format scripts. A +// broad extension-only filter would let Oxfmt rewrite evidence, fixtures, vendored sources, or +// generated output that the adopted 558-file parity experiment never selected. +const SELF_HOST_FORMAT_FILTER = + "grep -E '^((cli|gate-engine)/.*\\.(tsx?|jsx?|css|jsonc?|mjs|mts)|(tsconfig|biome)/.*\\.jsonc?|skills/.*\\.mjs|\\.co-occurrence-allowlist\\.json|\\.fallowrc\\.jsonc|\\.oxfmtrc\\.json|biome\\.jsonc|eslint\\.config\\.mjs|guard\\.config(\\.example)?\\.json|package\\.json|search-code\\.config\\.json|tsconfig(\\.build)?\\.json|vitest(\\.e2e)?\\.config\\.mjs|vitest\\.setup\\.mjs)$'"; // The `./dist/<...>.mjs` โ†’ `<...>.mts` transform pieces (hoisted โ€” useTopLevelRegex). const DIST_PREFIX_RE = /^\.\/dist\//; const MJS_EXT_RE = /\.mjs$/; @@ -100,9 +115,20 @@ export function sourceBinFor(cwd: string, binName: string): string { return distPath.replace(DIST_PREFIX_RE, '').replace(MJS_EXT_RE, '.mts'); } -/** Rewrite every `bunx guard-` in a generated hook to `node `. */ +/** Rewrite generated consumer commands to Devkit's self-host source/pinned-runtime equivalents. */ export function toSelfHost(hookText: string, cwd: string): string { - return hookText.replace(BUNX_GUARD_RE, (_m, bin: string) => `node ${sourceBinFor(cwd, bin)}`); + return ( + hookText + .replace(BUNX_GUARD_RE, (_m, bin: string) => `node ${sourceBinFor(cwd, bin)}`) + .replace(BUNX_BIOME_FORMAT_RE, 'node_modules/.bin/oxfmt --threads 1 --write') + .replace(BIOME_FORMAT_COMMENT_RE, 'Format staged files with Oxfmt') + // Formatting is a hard self-host responsibility now that Biome lint runs with formatting off. + // Generic consumer hooks retain their existing best-effort Biome behavior. + .replace(SELF_HOST_OXFMT_BEST_EFFORT, SELF_HOST_OXFMT_HARD) + // A replacement callback keeps the regex's terminal `$'` literal; replacement strings treat + // `$'` as the special token for the unmatched suffix. + .replace(BIOME_FORMAT_FILTER, () => SELF_HOST_FORMAT_FILTER) + ); } /** diff --git a/dist/README.md b/dist/README.md index e9aae1d4..4a6dd039 100644 --- a/dist/README.md +++ b/dist/README.md @@ -188,6 +188,7 @@ bun install --frozen-lockfile bun run devkit -- bun run test:run bun run typecheck +bun run format:check bun run lint bun run lint:structure bun run benchmarks:check diff --git a/dist/cli/lib/husky/self-host.mjs b/dist/cli/lib/husky/self-host.mjs index 54a06d21..0de1a924 100644 --- a/dist/cli/lib/husky/self-host.mjs +++ b/dist/cli/lib/husky/self-host.mjs @@ -25,11 +25,13 @@ import { readJson } from "../fs-helpers.mjs"; import { markEnd } from "./husky.mjs"; import { buildFullHook, buildGuardBlock, extractGuardBlock, replaceGuardBlock, } from "./husky-block.mjs"; // devkit's own structure-lint command (package.json `lint:structure` = `eslint cli gate-engine`) -// and its hard biome-lint gate (`lint` = `biome check --error-on-warnings .` โ€” biome exits 0 when +// and its hard Biome lint/assist gate (`lint` disables Biome's formatter explicitly โ€” Biome exits 0 when // every diagnostic is warn-severity, so without that flag the gate PRINTS its findings into the log // and passes the commit anyway). The hard-lint is folded into the deterministic orchestrator via // `--extra` (any non-zero blocks); both run via real devDeps -// (eslint/biome), so toSelfHost leaves them untouched. Together with the advisory fallow fragment +// (eslint/biome). Formatting is the one self-host-only rewrite beyond guard bins: Devkit has proven +// its pinned Oxfmt output over its own authored scope, while consumer hooks stay on Biome until each +// consumer completes the same parity exercise. Together with the advisory fallow fragment // below, the self-host hook preserves every gate the pre-self-host hand hook ran AND adds review + dup/clone. export const SELF_HOST_STRUCTURE_CMD = 'bun run lint:structure'; export const SELF_HOST_EXTRAS = [ @@ -58,8 +60,19 @@ else fi # /devkit:fallow-advisory`; // Matches the `bunx guard-` bins the generator emits. `guard-qavis-advisory` (double hyphen) is -// covered by `[a-z-]+`. `bunx biome` has no `guard-` prefix โ†’ correctly left alone. +// covered by `[a-z-]+`. The formatter has its own exact rewrite below so consumer output is not +// affected by the self-host-only Oxfmt adoption. const BUNX_GUARD_RE = /\bbunx (guard-[a-z-]+)\b/g; +const BUNX_BIOME_FORMAT_RE = /\bbunx biome format --write\b/g; +const BIOME_FORMAT_COMMENT_RE = /Format staged files with biome/g; +const BIOME_FORMAT_EXTENSIONS = '\\.(tsx?|jsx?|css|json|jsonc|mjs)$'; +const BIOME_FORMAT_FILTER = `grep -E '${BIOME_FORMAT_EXTENSIONS}'`; +const SELF_HOST_OXFMT_BEST_EFFORT = 'node_modules/.bin/oxfmt --threads 1 --write 2>/dev/null || true'; +const SELF_HOST_OXFMT_HARD = 'node_modules/.bin/oxfmt --threads 1 --write || exit 1'; +// Keep the staged hook on the same authored-file boundary as package.json's format scripts. A +// broad extension-only filter would let Oxfmt rewrite evidence, fixtures, vendored sources, or +// generated output that the adopted 558-file parity experiment never selected. +const SELF_HOST_FORMAT_FILTER = "grep -E '^((cli|gate-engine)/.*\\.(tsx?|jsx?|css|jsonc?|mjs|mts)|(tsconfig|biome)/.*\\.jsonc?|skills/.*\\.mjs|\\.co-occurrence-allowlist\\.json|\\.fallowrc\\.jsonc|\\.oxfmtrc\\.json|biome\\.jsonc|eslint\\.config\\.mjs|guard\\.config(\\.example)?\\.json|package\\.json|search-code\\.config\\.json|tsconfig(\\.build)?\\.json|vitest(\\.e2e)?\\.config\\.mjs|vitest\\.setup\\.mjs)$'"; // The `./dist/<...>.mjs` โ†’ `<...>.mts` transform pieces (hoisted โ€” useTopLevelRegex). const DIST_PREFIX_RE = /^\.\/dist\//; const MJS_EXT_RE = /\.mjs$/; @@ -82,9 +95,18 @@ export function sourceBinFor(cwd, binName) { throw new Error(`self-host: no bin "${binName}" in ${join(cwd, 'package.json')}`); return distPath.replace(DIST_PREFIX_RE, '').replace(MJS_EXT_RE, '.mts'); } -/** Rewrite every `bunx guard-` in a generated hook to `node `. */ +/** Rewrite generated consumer commands to Devkit's self-host source/pinned-runtime equivalents. */ export function toSelfHost(hookText, cwd) { - return hookText.replace(BUNX_GUARD_RE, (_m, bin) => `node ${sourceBinFor(cwd, bin)}`); + return (hookText + .replace(BUNX_GUARD_RE, (_m, bin) => `node ${sourceBinFor(cwd, bin)}`) + .replace(BUNX_BIOME_FORMAT_RE, 'node_modules/.bin/oxfmt --threads 1 --write') + .replace(BIOME_FORMAT_COMMENT_RE, 'Format staged files with Oxfmt') + // Formatting is a hard self-host responsibility now that Biome lint runs with formatting off. + // Generic consumer hooks retain their existing best-effort Biome behavior. + .replace(SELF_HOST_OXFMT_BEST_EFFORT, SELF_HOST_OXFMT_HARD) + // A replacement callback keeps the regex's terminal `$'` literal; replacement strings treat + // `$'` as the special token for the unmatched suffix. + .replace(BIOME_FORMAT_FILTER, () => SELF_HOST_FORMAT_FILTER)); } /** * The canonical devkit-dogfood selection: every recommended component + guard, PLUS `review` (the diff --git a/dist/package.json b/dist/package.json index 64909838..d4ac6631 100644 --- a/dist/package.json +++ b/dist/package.json @@ -58,13 +58,14 @@ "test:run": "vitest run", "test:e2e": "vitest run -c vitest.e2e.config.mjs", "playground": "bun scripts/playground.mts", - "lint": "biome check --error-on-warnings .", + "lint": "biome check --formatter-enabled=false --error-on-warnings .", "lint:structure": "eslint cli gate-engine", "benchmarks:check": "bun gate-engine/eval/cli.mts check", "benchmarks:render": "bun gate-engine/eval/cli.mts render", "benchmarks:typecheck": "tsc -p gate-engine/eval/tsconfig.json", "search-eval:check": "node gate-engine/search-tool/eval/eval.mts --fail", - "format": "biome check --write .", + "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", "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", diff --git a/docs/benchmarks/experiments/2026-08-16-oxfmt-devkit/README.md b/docs/benchmarks/experiments/2026-08-16-oxfmt-devkit/README.md new file mode 100644 index 00000000..a5f180ad --- /dev/null +++ b/docs/benchmarks/experiments/2026-08-16-oxfmt-devkit/README.md @@ -0,0 +1,133 @@ +# Devkit Oxfmt shadow migration โ€” 2026-08-16 + +**Verdict:** adopt Oxfmt 0.63.0 for Devkit's own formatting and retain Biome 2.5.6 for +lint/assist plus all consumer configuration and hooks. The hot self-host staged path invokes the +exact pinned Oxfmt package binary directly; `devkit oxc fmt` remains the portable entry point, but +its Node startup is not suitable for this latency-sensitive lane. + +This is a one-time migration experiment for Shortcut sc-1679, not a permanent benchmark or release +gate. It follows the staged boundary in +[`oxc-toolchain-migration`](../../../decisions/oxc-toolchain-migration.md). + +## What was compared + +| Item | Value | +| -------------------- | -------------------------------------------------------------------- | +| Source | clean Devkit commit `4d624d0` (the merged sc-1675 package runtime) | +| Host | macOS 26.5.1, arm64, 10 logical CPUs; host load uncontrolled | +| Runtime | Node 24.19.0; Bun 1.3.1 | +| Control | Biome 2.5.6 `format` over a Biome-formatted mirror | +| Candidate | Oxfmt 0.63.0 `--check` over an Oxfmt-formatted mirror | +| One-file proxy | check mode over byte-identical `cli/index.mts`; the hook uses write | +| Full scope | the same 558 authored files reported by Biome's configured allowlist | +| Sampling | 3 discarded warm-ups, then 10 measured samples; A/B order alternated | +| Statistics | median and nearest-rank p95; with 10 samples p95 is the maximum | + +Timing/CPU and RSS were collected in separate paired runs so the RSS sampler did not inflate wall +time. Wall time used a monotonic nanosecond clock. CPU used `wait4` user+system resource usage. +Process-tree RSS repeatedly sampled `ps` and summed the root plus every descendant at each snapshot; +the reported value is the largest observed sum. Changed runs received 2โ€“8 snapshots and full runs +5โ€“15. Installed dependencies were shared between mirrors and excluded from timing. + +## Configuration migration and parity + +`oxfmt --migrate biome` succeeded, found the root `biome.jsonc`, and emitted one warning: + +```text +"overrides" cannot be migrated automatically yet +``` + +That warning does not block formatter parity because Devkit's two root overrides only change +Biome linter severities. The generated formatter configuration was nevertheless unsafe to accept: +the migrator did not resolve `extends: ["./biome/base.jsonc"]`, so it produced tabs, 80 columns, and +double quotes instead of Devkit's spaces, 100 columns, and single quotes. + +The reviewed configuration corrects those inherited values and also: + +- disables Oxfmt's default `package.json` sorting, preserving the existing key order; +- overrides JSON and JSONC to use no trailing commas, matching Biome's JSON formatter; +- keeps `dist`, `templates`, `bun.lock`, and `node_modules` out of scope; and +- uses explicit authored-code globs rather than `oxfmt .`, which would additionally format Markdown, + YAML, vendored agent assets, generated evidence, and other languages that Biome did not own here. + +Biome reported 558 processed files. The shadow Oxfmt invocation processed the same 558 files. The +adopted command processes 559 because it additionally owns `.oxfmtrc.json` itself. Native coverage +is retained for the existing JavaScript/TypeScript, JSON/JSONC, and CSS patterns; generated `dist` +output and vendored prose remain deliberately excluded. Biome continues to own lint and assist via +`biome check --formatter-enabled=false`, so Oxfmt and Biome cannot produce competing formatting +verdicts. + +The old self-host staged-file filter used `tsx?`, which covers `.ts` and `.tsx` but not Devkit's +authored `.mts` files. The self-host-only rewrite adds `.mts` and has an execution test that formats +and re-stages a real staged `.mts` fixture. Generic consumer hooks keep their existing Biome filter; +this ticket does not silently broaden another repository's gate. + +## Complete one-time output review + +The corrected candidate changed 7 of 558 files. JSON, JSONC, configuration, lock, generated, and +vendored files were byte-identical. + +| File | Complete change classification | +| ----------------------------------------------------- | ----------------------------------------------------------- | +| `gate-engine/critique/capture-normalizer.mts` | wraps an interface's generic `extends Pick<...>` header | +| `gate-engine/decisions/__tests__/eval-bench.test.mts` | expands one long `it(...)` call and adds a trailing comma | +| `gate-engine/decisions/decision-format.mts` | aligns a multi-line Boolean expression | +| `gate-engine/eval/render.mts` | aligns a multi-line Boolean expression | +| `gate-engine/review/baseline-gate.mts` | removes one space before a `for` loop's closing parenthesis | +| `gate-engine/review/run-review.mts` | compacts two typed `.catch(...)` callbacks | +| `gate-engine/structure/walk.mts` | aligns a nested Boolean expression | + +Six files changed only in whitespace/layout; the expanded `eval-bench` call also gained Oxfmt's +permitted trailing comma. Identifiers, strings, operators, imports, comments, and control flow were +unchanged. After the first Oxfmt pass, a second 558-file write produced the same complete-tree +SHA-256 (`f65a73c1797731932485a7aa6aacce06932e872eaa0efd2ec65bb805b9db0879`). + +## Performance results + +CPU is the epic's primary metric. Wall time and complete-process-tree RSS are shown alongside it. + +| Scope / runner | Wall median / p95 | CPU median / p95 | Tree RSS median / p95 | +| ---------------------------------------- | ----------------: | ----------------: | --------------------: | +| One-file check ยท Biome direct | 0.0358 / 0.0561 s | 0.0357 / 0.0475 s | 39.7 / 45.8 MiB | +| One-file check ยท Oxfmt, 1 thread | 0.0827 / 0.1043 s | 0.0328 / 0.0531 s | 55.8 / 55.9 MiB | +| One-file check ยท wrapper, 1 thread | 0.1470 / 0.1896 s | 0.1113 / 0.1442 s | 137.0 / 137.1 MiB | +| Full ยท Biome direct | 0.1997 / 0.4054 s | 0.8709 / 1.0734 s | 143.7 / 144.2 MiB | +| Full ยท Oxfmt direct, default threads | 0.1242 / 0.1492 s | 0.2382 / 0.2700 s | 109.1 / 114.6 MiB | +| Full ยท Oxfmt direct, 1 thread | 0.2173 / 0.2712 s | 0.1667 / 0.2193 s | 68.4 / 68.6 MiB | +| Full ยท `devkit oxc fmt`, default threads | 0.1679 / 0.2259 s | 0.3027 / 0.3481 s | 192.0 / 195.8 MiB | + +Derived from medians: + +- the adopted direct full lane reduces CPU **72.6%**, wall time **37.8%**, and RSS **24.1%**; +- one-thread full mode reduces CPU **80.9%** and RSS **52.4%**, but increases wall time **8.8%**; +- the one-thread one-file check proxy reduces CPU **8.1%**, while wall grows 131.0% and RSS 40.7%; + this characterizes startup but is not presented as an exact write-mode hook speedup; and +- the same proxy shows the portable Node wrapper costs 211.8% more CPU and 245.3% more RSS than + Biome, so the self-host hook avoids that additional wrapper startup and uses the direct pinned + binary. + +The earlier A0 control measured changed Biome formatting at 0.140 s median wall, 0.060 s CPU, and +46.4 MiB RSS, and full formatting at 0.479 s wall, 0.970 s CPU, and 104.6 MiB RSS. Those values are +context rather than a paired comparison: this experiment changed the macOS build and re-ran both +engines on current pinned mirrors. The paired table above is the decision-grade comparison. + +## Adoption boundary + +This ticket changes only Devkit's own formatter: + +- `format` / `format:check` use the exact `oxfmt@0.63.0` dependency from sc-1675; +- CI runs `format:check` before the formatter-disabled Biome lint/assist gate, preserving the + server-side formatting verdict that `biome check` previously owned; +- the self-host staged hook uses direct Oxfmt with one thread, applies the same authored-path + allowlist as the full formatter, covers Devkit's `.mts` sources, and preserves partially staged + files; formatting errors fail the hook closed, and an execution fixture proves staged benchmark + evidence remains byte-identical. This guarantee applies to the index/worktree state observed at + hook start; concurrent writers to the same worktree are unsupported and agents must use isolated + worktrees; +- Biome remains the lint/assist owner with its formatter explicitly disabled in the hard gate; and +- generic package and standalone consumer hooks remain on Biome. Frink migration remains B3 and + must repeat this output/scope review on Frink rather than inheriting Devkit's result blindly. + +The seven reviewed layout changes are committed as the one-time formatter migration. Normal +typecheck, build, lint, structure, tests, self-host parity/doctor, and second-pass formatting are the +post-migration validation set. diff --git a/docs/decisions/oxc-toolchain-migration.md b/docs/decisions/oxc-toolchain-migration.md index be4e47f4..217c382e 100644 --- a/docs/decisions/oxc-toolchain-migration.md +++ b/docs/decisions/oxc-toolchain-migration.md @@ -20,3 +20,4 @@ created: 2026-08-15 **Scope:** package.json,biome.jsonc,eslint.config.mjs,guard.config.json,gate-engine/structure/**,eslint/**,docs/benchmarks/** **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-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. diff --git a/gate-engine/critique/capture-normalizer.mts b/gate-engine/critique/capture-normalizer.mts index cb66f62a..6a3b58ea 100644 --- a/gate-engine/critique/capture-normalizer.mts +++ b/gate-engine/critique/capture-normalizer.mts @@ -100,11 +100,10 @@ interface SanitizedText extends Sanitized { segments: Array<{ value: string; redacted: boolean }>; } -export interface PlanCritiqueCompletedCallbackV1 - extends Pick< - PlanCritiqueCaptureInputV1, - 'workId' | 'repository' | 'providerCompletedAt' | 'exactResponse' | 'opaqueTranscript' - > { +export interface PlanCritiqueCompletedCallbackV1 extends Pick< + PlanCritiqueCaptureInputV1, + 'workId' | 'repository' | 'providerCompletedAt' | 'exactResponse' | 'opaqueTranscript' +> { provider: PlanCritiqueProvider; callbackIdentity: string; model: string | null; diff --git a/gate-engine/decisions/__tests__/eval-bench.test.mts b/gate-engine/decisions/__tests__/eval-bench.test.mts index 6aa3ffe6..9c6ec7d9 100644 --- a/gate-engine/decisions/__tests__/eval-bench.test.mts +++ b/gate-engine/decisions/__tests__/eval-bench.test.mts @@ -377,24 +377,28 @@ describe('sub-benches (stubbed claude)', () => { }); // 3 fixture repos + 3 stub judges โ€” legitimately slow under full-suite load, hence the budget. - it('alignment: a partial outage scores that row NULL and continues; all-outage aborts (2)', { - timeout: 30000, - }, () => { - // The stub crashes only for the target whose ruling mentions "flaky". - useStub( - 'case "$*" in\n *"flaky ruling"*) exit 3;;\n *) printf "VERDICT: ALIGN\\n";;\nesac\n', - ); - const s = runAlignmentBench( - [alignRow('ok', 'stay generic', 'ALIGN'), alignRow('down', 'flaky ruling', 'ALIGN')], - { cascade: false }, - ); - expect(s.outages).toBe(1); - expect(s.results.find((r) => r.id === 'down').final).toBe('NULL'); - useStub('exit 3\n'); - expect(() => - runAlignmentBench([alignRow('all-down', 'stay generic', 'ALIGN')], { cascade: false }), - ).toThrow(BenchAbort); - }); + it( + 'alignment: a partial outage scores that row NULL and continues; all-outage aborts (2)', + { + timeout: 30000, + }, + () => { + // The stub crashes only for the target whose ruling mentions "flaky". + useStub( + 'case "$*" in\n *"flaky ruling"*) exit 3;;\n *) printf "VERDICT: ALIGN\\n";;\nesac\n', + ); + const s = runAlignmentBench( + [alignRow('ok', 'stay generic', 'ALIGN'), alignRow('down', 'flaky ruling', 'ALIGN')], + { cascade: false }, + ); + expect(s.outages).toBe(1); + expect(s.results.find((r) => r.id === 'down').final).toBe('NULL'); + useStub('exit 3\n'); + expect(() => + runAlignmentBench([alignRow('all-down', 'stay generic', 'ALIGN')], { cascade: false }), + ).toThrow(BenchAbort); + }, + ); }); // โ”€โ”€โ”€ Small-n statistics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/gate-engine/decisions/decision-format.mts b/gate-engine/decisions/decision-format.mts index b99aa250..3d1b9980 100644 --- a/gate-engine/decisions/decision-format.mts +++ b/gate-engine/decisions/decision-format.mts @@ -62,10 +62,10 @@ export interface TargetOptions extends AddOptions { export function hasTargetFields(options: AddOptions): options is TargetOptions { return Boolean( options.ruling && - options.context && - options.consequences && - options.tradeoff && - options.visionFit, + options.context && + options.consequences && + options.tradeoff && + options.visionFit, ); } diff --git a/gate-engine/eval/render.mts b/gate-engine/eval/render.mts index 9f0bab43..b785cb99 100644 --- a/gate-engine/eval/render.mts +++ b/gate-engine/eval/render.mts @@ -41,10 +41,10 @@ export function latestEvents(events: BenchmarkEvent[]): Map): boolean { return Boolean( a && - a.implementation === b.implementation && - a.corpus === b.corpus && - a.scorer === b.scorer && - a.runner === b.runner, + a.implementation === b.implementation && + a.corpus === b.corpus && + a.scorer === b.scorer && + a.runner === b.runner, ); } diff --git a/gate-engine/review/baseline-gate.mts b/gate-engine/review/baseline-gate.mts index 08ecba05..18c17853 100644 --- a/gate-engine/review/baseline-gate.mts +++ b/gate-engine/review/baseline-gate.mts @@ -112,7 +112,7 @@ export function parseNameStatusZ(raw: string): ChangedPath[] { const fields = raw.split(NUL); if (fields.at(-1) === '') fields.pop(); const changes: ChangedPath[] = []; - for (let i = 0; i < fields.length; ) { + for (let i = 0; i < fields.length;) { const status = fields[i++]; if (!status) fail('malformed empty status in staged name-status output'); const kind = status[0]; diff --git a/gate-engine/review/run-review.mts b/gate-engine/review/run-review.mts index 88fee997..97138d4b 100644 --- a/gate-engine/review/run-review.mts +++ b/gate-engine/review/run-review.mts @@ -216,14 +216,12 @@ export async function runReviewGate( const results = await mapLimit(plan.tasks, concurrency, (t, index) => { const t0 = Date.now(); return runCascade(t.sel, { ...baseOpts, recovery: assetRoot ? 'defer' : undefined }) - .catch( - (e): CascadeResult => ({ - name: t.sel.reviewer.name, - status: reviewMode ? 'error' : 'inconclusive', - reason: `engine error: ${e?.message ?? e}`, - escalated: false, - }), - ) + .catch((e): CascadeResult => ({ + name: t.sel.reviewer.name, + status: reviewMode ? 'error' : 'inconclusive', + reason: `engine error: ${e?.message ?? e}`, + escalated: false, + })) .then((outcome) => { const res = settleReviewOutcome(sctx, t, outcome, Date.now() - t0); const reason = retryableReason(res); @@ -241,14 +239,12 @@ export async function runReviewGate( retryFirst: false, // the deferred run IS the second chance โ€” never stack the outage retry checklistRecoveryReason: reason, recovery: 'final', - }).catch( - (e): CascadeResult => ({ - name: task.sel.reviewer.name, - status: reviewMode ? 'error' : 'inconclusive', - reason: `engine error: ${e?.message ?? e}`, - escalated: false, - }), - ), + }).catch((e): CascadeResult => ({ + name: task.sel.reviewer.name, + status: reviewMode ? 'error' : 'inconclusive', + reason: `engine error: ${e?.message ?? e}`, + escalated: false, + })), gateStart, ); if (progressFile) clearProgress(progressFile); // ran to completion โ†’ nothing unfinished to report diff --git a/gate-engine/structure/walk.mts b/gate-engine/structure/walk.mts index 340a2710..298906a4 100644 --- a/gate-engine/structure/walk.mts +++ b/gate-engine/structure/walk.mts @@ -67,8 +67,8 @@ export function walkTree(treeSpec: TreeSpec, absRoot: string, exts: string[]): s const fileOK = resolvePatterns(node.files, exts); const nodeBroken = Boolean( broken || - (node.enforceExistence && - !entries.some((e) => e.isFile() && e.name === node.enforceExistence)), + (node.enforceExistence && + !entries.some((e) => e.isFile() && e.name === node.enforceExistence)), ); for (const e of entries) { diff --git a/package.json b/package.json index 64909838..d4ac6631 100644 --- a/package.json +++ b/package.json @@ -58,13 +58,14 @@ "test:run": "vitest run", "test:e2e": "vitest run -c vitest.e2e.config.mjs", "playground": "bun scripts/playground.mts", - "lint": "biome check --error-on-warnings .", + "lint": "biome check --formatter-enabled=false --error-on-warnings .", "lint:structure": "eslint cli gate-engine", "benchmarks:check": "bun gate-engine/eval/cli.mts check", "benchmarks:render": "bun gate-engine/eval/cli.mts render", "benchmarks:typecheck": "tsc -p gate-engine/eval/tsconfig.json", "search-eval:check": "node gate-engine/search-tool/eval/eval.mts --fail", - "format": "biome check --write .", + "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", "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",