# Task orchestration for the repository gate.## `pnpm check` used to run seven stages in series (~6 min) and re-ran every one# of them even when a change could not possibly affect them. The stages here are# the same commands, still defined once in package.json, but mise runs them in# parallel and skips any whose declared `sources` have not changed since the# last successful run.## Rust stays on rustup: the Tauri targets, and the toolchain the release# workflow signs with, are managed there, and a second toolchain manager owning# the same `cargo` would only be a way to drift. [tools]# The same major as CI (.github/actions/setup-pnpm/action.yml, release.yml):# a test that passes on a newer local Node can still fail there.node = "22"pnpm = "10.13.1" # Vitest, tsc, and cargo each saturate several cores on their own, so the four# heavy stages are already enough to fill a laptop. Raising this mostly makes# every stage slower.[settings]jobs = 4 [tasks.i18n-check]description = "Extract catalogs, fail on drift, compile, and run the coverage guard"run = "pnpm i18n:check"# The catalogs are both an input and an output of this stage. Compiled `.js`# catalogs are generated, so they must not feed the freshness check.sources = [ "src/**/*.{ts,tsx}", "!src/locales/**/*.js", "lingui.config.ts", "src/locales/**/*.po", "scripts/check-i18n-coverage.mjs", "scripts/i18n-unlocalized-baseline.txt", ".oxlintrc.json",] # knip rides along: it reads the same trees plus scripts/, and keeps unused# files, dependencies and exports from creeping back in. So does# perf:evidence, which recomputes the benchmark tables in docs/performance*.md from# their retained runs (scripts/check-perf-evidence.mjs).[tasks.lint]description = "Oxlint over src and tools (the warning cap is a debt ratchet — lower it, never raise it), knip, and the benchmark-table evidence check"run = ["pnpm lint", "pnpm knip", "pnpm perf:evidence"]sources = [ "src/**", "docs/performance.md", "docs/performance-history.md", "docs/performance-data/**", "!src/locales/**/*.js", "tools/**", "scripts/**", "knip.json", ".oxlintrc.json", "tsconfig.json", "package.json", "pnpm-lock.yaml",] [tasks.test]description = "Vitest suite"run = "pnpm test"sources = [ "src/**", "!src/locales/**/*.js", # Vitest also discovers script-level tests, whose imported implementation # modules must invalidate this stage just like src/ does. "scripts/**", # scripts/agent-quality-eval.test.ts grades the committed agent transcripts. "evals/**", # Compatibility tests execute the shipping script and check its HTML order. "public/polyfills.js", "index.html", # Invariant tests read the Rust sources and embedded skills, the Tauri # config and capabilities, the tutorial templates and fixtures, and the # shipped notices (tauri-security-config, window-layout, native-locale, # open-slide-skill, board-store, third-party-notices, ...). "src-tauri/src/**", "src-tauri/tauri.conf.json", "src-tauri/capabilities/**", "src-tauri/templates/**", "src-tauri/tests/fixtures/**", "THIRD_PARTY_NOTICES.md", "vitest.config.ts", "tsconfig.json", "package.json", "pnpm-lock.yaml",] [tasks.build]description = "Typecheck and produce the production web bundle"run = "pnpm build"sources = [ "src/**", "!src/locales/**/*.js", "index.html", # `tools/` is not a build input, but tsc typechecks it (tsconfig.json # `include`), and this stage runs tsc before vite. "tools/**", "vite.config.ts", # The Timeless font plugin vite.config.ts imports (the fonts themselves live # outside the checkout, so a new copy of them does not invalidate this stage). "scripts/private-fonts.ts", # The React Compiler suppression list vite.config.ts imports. "scripts/react-compiler-suppressions.ts", "tsconfig.json", "tsconfig.node.json", "package.json", "pnpm-lock.yaml", "public/**", "scripts/app-size-report.mjs", "scripts/app-size-report-selftest.mjs", "scripts/check-webkit-compat.mjs", # Copied out of pdfjs-dist by vite.config.ts on every build; an input that the # build itself rewrites can never be fresh. "!public/pdfjs/**",]outputs = ["dist"] # The public literature Worker is a separate pnpm project with its own# lockfile, so the root install never sees it and none of the stages above# cover it. Its typecheck and test are not root package.json scripts, so they# are spelled out here rather than wrapped.## The tests run on @cloudflare/vitest-pool-workers, which starts a local workerd# and simulates the bindings declared in wrangler.jsonc. No Cloudflare account# or `wrangler login` is needed; only a deploy talks to Cloudflare.[tasks.literature-worker]description = "Typecheck and test the literature Worker (its own pnpm project)"run = [ # Its own lockfile, so a fresh worktree has none of its packages yet. "pnpm --dir literature-worker install --frozen-lockfile", "pnpm --dir literature-worker typecheck", "pnpm --dir literature-worker test",]sources = [ "literature-worker/src/**", "literature-worker/test/**", "literature-worker/tsconfig.json", "literature-worker/vitest.config.ts", "literature-worker/wrangler.jsonc", "literature-worker/package.json", "literature-worker/pnpm-lock.yaml",] [tasks.cargo-fmt]description = "rustfmt check (CI enforces this; it used to be missing from the local gate)"run = "cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check"sources = ["src-tauri/**/*.rs", "!src-tauri/target/**"] [tasks.cargo-test]description = "Rust unit and integration tests"run = "cargo test --manifest-path src-tauri/Cargo.toml"sources = [ "src-tauri/**/*.rs", "src-tauri/src/**/*.c", "src-tauri/src/**/*.sh", "!src-tauri/target/**", "src-tauri/Cargo.toml", "src-tauri/Cargo.lock", "src-tauri/tauri.conf.json", "src-tauri/capabilities/**", # Compiled in through include_str!/include_bytes!: the templates, the test # fixtures, and the frontend files macos_window.rs pins the title bar to. "src-tauri/templates/**", "src-tauri/tests/fixtures/**", "src/app/use-native-window.ts", "src/App.css", "src/styles/app-shell.css", "src/styles/foundations.css",] [tasks.clippy]description = "Clippy with warnings denied"run = "cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings"sources = [ "src-tauri/**/*.rs", "src-tauri/src/**/*.c", "src-tauri/src/**/*.sh", "!src-tauri/target/**", "src-tauri/Cargo.toml", "src-tauri/Cargo.lock", "src-tauri/tauri.conf.json", "src-tauri/capabilities/**", # Compiled in through include_str!/include_bytes!: the templates, the test # fixtures, and the frontend files macos_window.rs pins the title bar to. "src-tauri/templates/**", "src-tauri/tests/fixtures/**", "src/app/use-native-window.ts", "src/App.css", "src/styles/app-shell.css", "src/styles/foundations.css",] # Aggregated third-party notices. ~2 s, which is why it belongs here rather than# in the "run it by hand and hope" category: THIRD_PARTY_NOTICES.md is a legal# artifact, and the way it rots is that someone adds a dependency and nobody# regenerates.## `--allow-unresolved` is deliberate. The generator exits 2 when a package# declares a license but ships no text and none was found in its repository# either (`pnpm notices --fetch-upstream` recovers the rest into# licenses/upstream/). That is a standing condition recorded in the file itself,# not drift, and a gate that is red on every run is a gate people learn to# ignore. This stage checks only that# the generated section matches the installed trees; run `pnpm notices` to see# the gaps, which it still prints in full.## The sidecar closure is skipped unless `src-tauri/synara-runtime/` happens to be# staged from the current pin, since `pnpm check` normally runs against the# placeholder stub. When it is absent or stale the previously generated section# is carried over untouched and only the npm and crates closures are verified —# the generator says so on stderr.## CI runs the same check in its `lint-and-build` job.[tasks.notices]description = "Check THIRD_PARTY_NOTICES.md against the installed npm and crates trees"run = "pnpm notices:check"# The closures are read from node_modules and the cargo registry, so the# lockfiles are the freshness signal; the generated output and the vendored# license texts are inputs too, because editing either is drift.sources = [ "package.json", "pnpm-lock.yaml", "src-tauri/Cargo.toml", "src-tauri/Cargo.lock", "scripts/generate-notices.mjs", "scripts/lib/util.mjs", "scripts/lib/package-notices.mjs", "scripts/lib/license-classification.mjs", "scripts/lib/license-grouping.mjs", "scripts/lib/dependency-closures.mjs", "scripts/lib/upstream-licenses.mjs", "public/licenses/**", "licenses/**", "THIRD_PARTY_NOTICES.md",] # The interaction benchmark CI gates on (docs/performance.md). Not part of# `check`: it drives a local Chrome, then Playwright's WebKit# (`pnpm exec playwright-core install webkit` once), for a few minutes each,# and its counts are meant to be read on an otherwise idle machine.[tasks.perf-bench]description = "Interaction benchmark: fail when a gated count exceeds its ceiling in scripts/perf-bench/budgets.json (Chromium) or budgets-webkit.json (WebKit), or a layout check fails in either engine"run = ["pnpm perf:bench --check --layout", "pnpm perf:bench --check --layout --engine webkit"] # The Open Slide presentation server is another nested pnpm project, staged# into the app by scripts/prepare-presentation-runtime.mjs. Its tests run on# node:test against server.mjs; the install is frozen and fast, so it runs# here rather than being a separate setup step to forget. Staging prunes the# installed tree and then serves the editor from it, so it proves the pruned# runtime still has everything the editor, decks, and exports load.[tasks.open-slide-runtime]description = "Test the Open Slide presentation server (its own pnpm project) and stage its pruned runtime"run = [ "pnpm --dir tools/open-slide-runtime install --frozen-lockfile", "pnpm --dir tools/open-slide-runtime test", "node scripts/prepare-presentation-runtime.mjs",]sources = [ "tools/open-slide-runtime/*.mjs", "tools/open-slide-runtime/package.json", "tools/open-slide-runtime/pnpm-lock.yaml", "tools/open-slide-runtime/patches/**", "src-tauri/src/presentation.rs", "scripts/prepare-presentation-runtime.mjs", "scripts/check-presentation-runtime.mjs", "src-tauri/templates/tutorial/**",] [tasks.check]description = "THE gate: everything CI runs, in parallel, skipping unchanged stages"depends = [ "i18n-check", "lint", "test", "build", "literature-worker", "open-slide-runtime", "cargo-fmt", "cargo-test", "clippy", "notices",]
# Task orchestration for the repository gate.
#
# `pnpm check` used to run seven stages in series (~6 min) and re-ran every one
# of them even when a change could not possibly affect them. The stages here are
# the same commands, still defined once in package.json, but mise runs them in
# parallel and skips any whose declared `sources` have not changed since the
# last successful run.
#
# Rust stays on rustup: the Tauri targets, and the toolchain the release
# workflow signs with, are managed there, and a second toolchain manager owning
# the same `cargo` would only be a way to drift.
[tools]
# The same major as CI (.github/actions/setup-pnpm/action.yml, release.yml):
# a test that passes on a newer local Node can still fail there.
node = "22"
pnpm = "10.13.1"
# Vitest, tsc, and cargo each saturate several cores on their own, so the four
# heavy stages are already enough to fill a laptop. Raising this mostly makes
# every stage slower.
[settings]
jobs = 4
[tasks.i18n-check]
description = "Extract catalogs, fail on drift, compile, and run the coverage guard"
run = "pnpm i18n:check"
# The catalogs are both an input and an output of this stage. Compiled `.js`
# catalogs are generated, so they must not feed the freshness check.
sources = [
"src/**/*.{ts,tsx}",
"!src/locales/**/*.js",
"lingui.config.ts",
"src/locales/**/*.po",
"scripts/check-i18n-coverage.mjs",
"scripts/i18n-unlocalized-baseline.txt",
".oxlintrc.json",
]
# knip rides along: it reads the same trees plus scripts/, and keeps unused
# files, dependencies and exports from creeping back in. So does
# perf:evidence, which recomputes the benchmark tables in docs/performance*.md from
# their retained runs (scripts/check-perf-evidence.mjs).
[tasks.lint]
description = "Oxlint over src and tools (the warning cap is a debt ratchet — lower it, never raise it), knip, and the benchmark-table evidence check"
run = ["pnpm lint", "pnpm knip", "pnpm perf:evidence"]
sources = [
"src/**",
"docs/performance.md",
"docs/performance-history.md",
"docs/performance-data/**",
"!src/locales/**/*.js",
"tools/**",
"scripts/**",
"knip.json",
".oxlintrc.json",
"tsconfig.json",
"package.json",
"pnpm-lock.yaml",
]
[tasks.test]
description = "Vitest suite"
run = "pnpm test"
sources = [
"src/**",
"!src/locales/**/*.js",
# Vitest also discovers script-level tests, whose imported implementation
# modules must invalidate this stage just like src/ does.
"scripts/**",
# scripts/agent-quality-eval.test.ts grades the committed agent transcripts.
"evals/**",
# Compatibility tests execute the shipping script and check its HTML order.
"public/polyfills.js",
"index.html",
# Invariant tests read the Rust sources and embedded skills, the Tauri
# config and capabilities, the tutorial templates and fixtures, and the
# shipped notices (tauri-security-config, window-layout, native-locale,
# open-slide-skill, board-store, third-party-notices, ...).
"src-tauri/src/**",
"src-tauri/tauri.conf.json",
"src-tauri/capabilities/**",
"src-tauri/templates/**",
"src-tauri/tests/fixtures/**",
"THIRD_PARTY_NOTICES.md",
"vitest.config.ts",
"tsconfig.json",
"package.json",
"pnpm-lock.yaml",
]
[tasks.build]
description = "Typecheck and produce the production web bundle"
run = "pnpm build"
sources = [
"src/**",
"!src/locales/**/*.js",
"index.html",
# `tools/` is not a build input, but tsc typechecks it (tsconfig.json
# `include`), and this stage runs tsc before vite.
"tools/**",
"vite.config.ts",
# The Timeless font plugin vite.config.ts imports (the fonts themselves live
# outside the checkout, so a new copy of them does not invalidate this stage).
"scripts/private-fonts.ts",
# The React Compiler suppression list vite.config.ts imports.
"scripts/react-compiler-suppressions.ts",
"tsconfig.json",
"tsconfig.node.json",
"package.json",
"pnpm-lock.yaml",
"public/**",
"scripts/app-size-report.mjs",
"scripts/app-size-report-selftest.mjs",
"scripts/check-webkit-compat.mjs",
# Copied out of pdfjs-dist by vite.config.ts on every build; an input that the
# build itself rewrites can never be fresh.
"!public/pdfjs/**",
]
outputs = ["dist"]
# The public literature Worker is a separate pnpm project with its own
# lockfile, so the root install never sees it and none of the stages above
# cover it. Its typecheck and test are not root package.json scripts, so they
# are spelled out here rather than wrapped.
#
# The tests run on @cloudflare/vitest-pool-workers, which starts a local workerd
# and simulates the bindings declared in wrangler.jsonc. No Cloudflare account
# or `wrangler login` is needed; only a deploy talks to Cloudflare.
[tasks.literature-worker]
description = "Typecheck and test the literature Worker (its own pnpm project)"
run = [
# Its own lockfile, so a fresh worktree has none of its packages yet.
"pnpm --dir literature-worker install --frozen-lockfile",
"pnpm --dir literature-worker typecheck",
"pnpm --dir literature-worker test",
]
sources = [
"literature-worker/src/**",
"literature-worker/test/**",
"literature-worker/tsconfig.json",
"literature-worker/vitest.config.ts",
"literature-worker/wrangler.jsonc",
"literature-worker/package.json",
"literature-worker/pnpm-lock.yaml",
]
[tasks.cargo-fmt]
description = "rustfmt check (CI enforces this; it used to be missing from the local gate)"
run = "cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check"
sources = ["src-tauri/**/*.rs", "!src-tauri/target/**"]
[tasks.cargo-test]
description = "Rust unit and integration tests"
run = "cargo test --manifest-path src-tauri/Cargo.toml"
sources = [
"src-tauri/**/*.rs",
"src-tauri/src/**/*.c",
"src-tauri/src/**/*.sh",
"!src-tauri/target/**",
"src-tauri/Cargo.toml",
"src-tauri/Cargo.lock",
"src-tauri/tauri.conf.json",
"src-tauri/capabilities/**",
# Compiled in through include_str!/include_bytes!: the templates, the test
# fixtures, and the frontend files macos_window.rs pins the title bar to.
"src-tauri/templates/**",
"src-tauri/tests/fixtures/**",
"src/app/use-native-window.ts",
"src/App.css",
"src/styles/app-shell.css",
"src/styles/foundations.css",
]
[tasks.clippy]
description = "Clippy with warnings denied"
run = "cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings"
sources = [
"src-tauri/**/*.rs",
"src-tauri/src/**/*.c",
"src-tauri/src/**/*.sh",
"!src-tauri/target/**",
"src-tauri/Cargo.toml",
"src-tauri/Cargo.lock",
"src-tauri/tauri.conf.json",
"src-tauri/capabilities/**",
# Compiled in through include_str!/include_bytes!: the templates, the test
# fixtures, and the frontend files macos_window.rs pins the title bar to.
"src-tauri/templates/**",
"src-tauri/tests/fixtures/**",
"src/app/use-native-window.ts",
"src/App.css",
"src/styles/app-shell.css",
"src/styles/foundations.css",
]
# Aggregated third-party notices. ~2 s, which is why it belongs here rather than
# in the "run it by hand and hope" category: THIRD_PARTY_NOTICES.md is a legal
# artifact, and the way it rots is that someone adds a dependency and nobody
# regenerates.
#
# `--allow-unresolved` is deliberate. The generator exits 2 when a package
# declares a license but ships no text and none was found in its repository
# either (`pnpm notices --fetch-upstream` recovers the rest into
# licenses/upstream/). That is a standing condition recorded in the file itself,
# not drift, and a gate that is red on every run is a gate people learn to
# ignore. This stage checks only that
# the generated section matches the installed trees; run `pnpm notices` to see
# the gaps, which it still prints in full.
#
# The sidecar closure is skipped unless `src-tauri/synara-runtime/` happens to be
# staged from the current pin, since `pnpm check` normally runs against the
# placeholder stub. When it is absent or stale the previously generated section
# is carried over untouched and only the npm and crates closures are verified —
# the generator says so on stderr.
#
# CI runs the same check in its `lint-and-build` job.
[tasks.notices]
description = "Check THIRD_PARTY_NOTICES.md against the installed npm and crates trees"
run = "pnpm notices:check"
# The closures are read from node_modules and the cargo registry, so the
# lockfiles are the freshness signal; the generated output and the vendored
# license texts are inputs too, because editing either is drift.
sources = [
"package.json",
"pnpm-lock.yaml",
"src-tauri/Cargo.toml",
"src-tauri/Cargo.lock",
"scripts/generate-notices.mjs",
"scripts/lib/util.mjs",
"scripts/lib/package-notices.mjs",
"scripts/lib/license-classification.mjs",
"scripts/lib/license-grouping.mjs",
"scripts/lib/dependency-closures.mjs",
"scripts/lib/upstream-licenses.mjs",
"public/licenses/**",
"licenses/**",
"THIRD_PARTY_NOTICES.md",
]
# The interaction benchmark CI gates on (docs/performance.md). Not part of
# `check`: it drives a local Chrome, then Playwright's WebKit
# (`pnpm exec playwright-core install webkit` once), for a few minutes each,
# and its counts are meant to be read on an otherwise idle machine.
[tasks.perf-bench]
description = "Interaction benchmark: fail when a gated count exceeds its ceiling in scripts/perf-bench/budgets.json (Chromium) or budgets-webkit.json (WebKit), or a layout check fails in either engine"
run = ["pnpm perf:bench --check --layout", "pnpm perf:bench --check --layout --engine webkit"]
# The Open Slide presentation server is another nested pnpm project, staged
# into the app by scripts/prepare-presentation-runtime.mjs. Its tests run on
# node:test against server.mjs; the install is frozen and fast, so it runs
# here rather than being a separate setup step to forget. Staging prunes the
# installed tree and then serves the editor from it, so it proves the pruned
# runtime still has everything the editor, decks, and exports load.
[tasks.open-slide-runtime]
description = "Test the Open Slide presentation server (its own pnpm project) and stage its pruned runtime"
run = [
"pnpm --dir tools/open-slide-runtime install --frozen-lockfile",
"pnpm --dir tools/open-slide-runtime test",
"node scripts/prepare-presentation-runtime.mjs",
]
sources = [
"tools/open-slide-runtime/*.mjs",
"tools/open-slide-runtime/package.json",
"tools/open-slide-runtime/pnpm-lock.yaml",
"tools/open-slide-runtime/patches/**",
"src-tauri/src/presentation.rs",
"scripts/prepare-presentation-runtime.mjs",
"scripts/check-presentation-runtime.mjs",
"src-tauri/templates/tutorial/**",
]
[tasks.check]
description = "THE gate: everything CI runs, in parallel, skipping unchanged stages"
depends = [
"i18n-check",
"lint",
"test",
"build",
"literature-worker",
"open-slide-runtime",
"cargo-fmt",
"cargo-test",
"clippy",
"notices",
]