# Local verification

| Command                 | What success proves                                                | Excludes                                                                                                                         |
| ----------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `vp run check:affected` | Selected gates passed, including any tests they contain            | Unselected scopes, verification-only suites and builds, and CI-only checks                                                       |
| `vp run check:push`     | Selected static checks, prose checks and focused unit tests passed | Complete Node suites, Java unit and integration tests, browser suites, builds and live-service tests                             |
| `vp run check`          | Every gate the `quality` task in `vite.config.ts` runs passed      | Distributable builds, browser and live-service tests, images, mutation analysis, and release security                            |
| `vp run verify`         | The local verification suites and builds passed                    | Server integration and database tests, full-application E2E, live-service tests, images, mutation analysis, and release security |

While you work, run `check:affected`. Before you push, let the pre-push hook run `check:push`: it runs the checks that pull request CI runs for the changed trees. Run `check` when you change a shared input, such as the lockfile, or before you push another local ref.

`check` validates the contents of all pending changeset files through `gate:changesets`. The pre-push hook also applies CI's release-note rules, on every push, against the merge base with `origin/main` as last fetched. A branch must not edit `MIGRATION.md`, or delete or rename a pending changeset or migration fragment. A change to shipped code needs an added changeset. CI applies the same rules against the tested merge base, and exempts drafts, bots and verified reverts.

Before you request review, run `verify`. `vp run` with no task name lists every task, and `vite.config.ts` is where each one is defined.

On first use, the script tests in `check` download two pinned tools, Vale and oasdiff, into a cache that every worktree of the clone shares.

## Dependency installation

After changing dependency declarations, the lockfile, package-manager settings, or declared patches, reconcile the installed dependencies before local gates. This includes changes from a branch switch, rebase, or merge.

```
vp install --frozen-lockfile
```

The native package manager reconciles the installed graph and checks declared patches. A frozen install preserves the lockfile. See [Local development](/next/contributor/local-development.md#prerequisites) for the pinned toolchain.

## Rebase generated artifacts

For a pull request in this repository that targets `main`, use:

```
vp run pr:rebase-generated <pr-number>
```

Use Git 2.38 or later. Get the branch owner's consent before you rewrite their branch. Ask the queue owner to dequeue the pull request first. Check out its branch at the remote head, with a clean worktree and index. Install its dependencies first with the frozen command above. Unset `HEPHAESTUS_APPLICATION_JAR`.

The command refuses an existing Git operation, merge commits, a queued pull request, or a different local head. If an older branch does not have this command, run its script from a current checkout:

```
node /path/to/current-checkout/scripts/rebase-generated.ts <pr-number>
```

Run that command from the pull request's worktree. The build and generators use the rebased worktree's code and dependencies.

The command fetches `main` and records its commit. It rebases onto that commit and restores the original branch if a handwritten file conflicts. It resolves conflicts only in `server/openapi.yaml`, both generated API client directories, and the configuration reference.

It also moves this branch's migrations after the newest migration on `main` and in the merge queue, as `vp run db:reorder` does. The rename is staged beside the regenerated files.

Then it reconciles dependencies with a frozen install and builds one fresh server JAR. That JAR supplies the spec and configuration metadata, and both clients are regenerated from the spec. The command does not push or change the merge queue.

Review the regenerated diff and compare the commits with `git range-diff`. The command reports any generated-only commit it drops during conflict resolution. Commit the regenerated files, then run `format`, `check`, and the tests for the affected paths. Use the original head that the command prints as the explicit push lease.

If generation fails, the branch stays rebased and partial output stays available for diagnosis. Fix the failure and unset `HEPHAESTUS_APPLICATION_JAR`. Run `vp run generate:api` and `vp run generate:configuration-reference` before you commit or push.

## Affected selection

The command compares committed, staged, unstaged, untracked, and deleted paths with the merge base of `origin/main`. For another target, run `vp run check:affected --base <revision>`.

| Changed path                                                                                                                                                                                                     | Task                                                                    |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `webapp/**`                                                                                                                                                                                                      | `affected:webapp`                                                       |
| `server/**` outside the agent and practice script resources                                                                                                                                                      | `affected:server`                                                       |
| `server/**/resources/{agent,practices/scripts}/**`, `docker/agents/**`                                                                                                                                           | `affected:agents`                                                       |
| `docs/**`                                                                                                                                                                                                        | `affected:docs`                                                         |
| `docs/images/readme/**`                                                                                                                                                                                          | `affected:docs` and `affected:webapp`                                   |
| `.changeset/*.md` except `README.md`                                                                                                                                                                             | `affected:changesets`: release-note validation, Markdown lint and prose |
| `.github/**`, `.claude/**`, `.agents/**`, other `.changeset/**` files, `scripts/**`, `webapp/tools/oxlint/**`, root package and tsconfig files, generated artifacts, `AGENTS.md`, `CLAUDE.md`, or any other path | `quality`                                                               |

The tasks are defined in `vite.config.ts`. Selection uses `git diff --no-renames`, so both sides of a rename are evaluated. Git documents the underlying change statuses in [`git diff`](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB).

## Pre-push hook

Git runs the hooks in the tracked `.vite-hooks/` directory. Every worktree of a clone shares `core.hooksPath`, and Git resolves a relative path against the worktree that runs the hook ([`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath)). `vp install` sets the path to `.vite-hooks`, which exists in every checkout. Thus, a new worktree runs its own hooks from its first checkout, before any install.

The install replaces a path to this repository's hooks in one worktree of the clone or in a removed checkout. It also replaces a path to the dispatcher that Vite+ generates in each worktree. An absolute path runs one worktree's hooks and `vp` in every other worktree. Git runs no hook, without a warning, in a worktree where the dispatcher is missing.

The install also removes each worktree's own `core.hooksPath` override that names these hooks, in every worktree of the clone. Thus, no worktree depends on which checkout last ran an install. The install keeps a contributor's own hooks directory, also one named `.vite-hooks` in another project. Do not set `core.hooksPath` by hand.

Each hook starts through `.vite-hooks/run`, which runs the `vp` that the install of that worktree put in its `node_modules`. In a worktree without an install, the hook refuses the commit or push and says to install, instead of skipping. The install and `vp run check:affected` warn when the hooks path that Git uses in a worktree does not exist.

To skip the hooks once, set `VP_GIT_HOOKS=0`. `git config vp.hooks.disabled true` turns them off in a clone, and the install keeps that setting. Do not run `vp hooks enable`: it points every worktree back at the generated dispatcher. A worktree on a branch from before the tracked launcher runs its old hook directly, so it needs `vp` on `PATH` until you rebase it.

The pre-push hook runs `vp run check:push` with one task worker. It compares the same paths as `check:affected` and runs selected format, lint, TypeScript and prose checks. It does not write source files. `git push --no-verify` skips the hook. A change under `db/changelog/next/` first checks that the branch's migrations sort after `origin/main` as last fetched and after the merge queue's. If the hook cannot read the queue, it says so and checks against `origin/main` only.

For Node, it runs changed test files, existing source companion tests and the CI contract suites for workflow changes. This selection does not cover every fixture or resource input. The complete Node suites remain in `check` and CI.

A server change runs one Gradle invocation: Spotless, PMD, the test compilation and the architecture tier. The Java unit and integration tiers remain in CI. Instruction changes also run the instruction gate. The workflow parser and CI linters remain outside the hook.

For frontend source files, the hook runs the changed test files and the colocated test of each changed source, such as `Dialog.test.tsx` beside `Dialog.tsx`. It does not follow the import graph: through the router, one component reaches most route tests, which take minutes on one worker. CI runs the complete unit project. A source without a colocated test runs no test here. The hook then prints that CI runs the tests that reach the change through imports.

Shared inputs, test helpers, network mocks, deleted files and non-code resources select the complete unit project. The extension selects its unit project only. Each test runner uses one worker. If a frontend unit run prints nothing for five minutes, the hook stops it and fails. On Linux and macOS, each Node process of the run first writes a diagnostic report, and the hook prints their directory.

A webapp change also builds the webapp, which enforces the bundle budget. The build takes seconds, and the browser suites in CI build the same bundle.

A change under `scripts/` selects the tooling checks and each tree whose CI path filter in `cicd.yml` names the file. Pull request CI runs the same trees for that change.

An unknown path or missing merge base selects every scoped check and both frontend unit projects. The hook does not fetch, install tools, start a browser or build an artifact other than the webapp bundle. If a pinned tool is absent from the shared cache, run `vp run check` once in any worktree of the clone. The hook reports its selected scope and the suites that still require the full gate or CI. It checks the current worktree, its HEAD and pending changes. Before you push another local ref, run the full gate on that exact commit.

## Complete local verification

`vp run verify` runs `quality`, then these suites and builds:

1. The webapp unit tests.
2. The Storybook browser tests.
3. `test:server:selection`, followed by `test:server:verification` (unit and architecture with coverage).
4. The webapp build, with a `routeTree.gen.ts` comparison that restores the committed file before the command exits. It also builds the commit that the change merges onto and holds the bundle budget against it (`webapp/AGENTS.md` § Routing).
5. The Storybook build.
6. The docs build.

Server integration and database tests, full-application E2E, live-service tests, image builds, mutation analysis, and release security are outside `verify`. See [CI/CD](/next/contributor/ci-cd.md). Run affected server integration tests separately with `vp run test:server:integration`.

### Application and Storybook bundles

The application keeps Vite's 500 kB chunk warning limit. The floating Hephaestus panel is loaded behind its feature boundary. React's shared renderer has a separate cacheable chunk.

Storybook is a development tool, not the application bundle. Its build checks every JavaScript chunk: ordinary chunks must stay at or below 500,000 bytes. Two monolithic upstream modules have separate raw and gzip budgets in `webapp/.storybook/chunk-budgets.ts`:

| Upstream module                                                      | Raw bytes | Gzip bytes |
| -------------------------------------------------------------------- | --------- | ---------- |
| [Storybook preview runtime](https://www.npmjs.com/package/storybook) | 1,186,100 | 339,000    |
| [axe-core](https://www.npmjs.com/package/axe-core)                   | 672,000   | 190,000    |

The story index lists every story with its preload dependencies, so it grows with each story. It is application code, so it has its own ordinary chunk and does not count against the preview runtime budget.

The build prints the actual sizes against these budgets. It fails on an unknown oversized chunk or when either limit is exceeded, rather than repeating Vite's undifferentiated size warning. Module identity, not a filename such as `iframe` or `axe`, selects the exception. An upstream upgrade that outgrows a budget needs fresh attribution before a budget change. Removing accessibility checks or collecting application code into a tool chunk is not a size fix.

## Vite+ task cache

On Windows the runner's automatic input tracking misses a file you add, so run `vp run --no-cache check` there.

`quality` is a group: its gates run in parallel, and a cacheable gate whose inputs are unchanged replays. `vite.config.ts` states which gates are uncached and which name their inputs. Run `vp run --last-details` to inspect the latest run and its miss reasons, or `vp cache clean` to discard the local cache. In CI the cache restores per [CI/CD](/next/contributor/ci-cd.md#caches).

### Java checks and caching

`gate:server` invokes Gradle once for build-script formatting, Java formatting and PMD. Gradle owns their inputs and cached outputs. Vite does not cache a second verdict over the Java build. The scoped commands `format:java:check` and `lint:java` use the same native tasks. Test execution remains uncached.

Gradle reports individual deprecations through `org.gradle.warning.mode=all`. The pinned [GraphQL code-generation plugin](https://github.com/kobylynskyi/graphql-java-codegen/blob/v5.10.0/plugins/gradle/graphql-java-codegen-gradle-plugin/src/main/java/io/github/kobylynskyi/graphql/codegen/gradle/GraphQLCodegenGradleTask.java#L1022) still exposes nullable `Boolean isX()` properties that Gradle 10 will stop recognizing. This needs upstream `getX()` accessors preserving nullable semantics, not primitive defaults or disabled warnings. Cached generation can omit these diagnostics. A quiet warm run does not prove that the dependency is repaired.

`vp run --no-cache check` disables Vite's cache, not Gradle's. To inspect a Java check without task-output or configuration-cache reuse, run:

```
./server/gradlew -p server :spotlessCheck :application:spotlessCheck :application:pmdMain \

  --rerun-tasks --no-build-cache --no-configuration-cache --profile
```

The generated HTML report is `server/build/reports/profile/profile-*.html`. Keep clean, warm and cache-restored measurements separate. [Gradle's performance guide](https://docs.gradle.org/current/userguide/performance.html) explains why a baseline is necessary before a build change.

PMD must analyze every selected source successfully: a report containing processing or configuration errors fails the task, even when it contains no rule violations. `vp run gate:pmd-canary` proves this with clean, rule-violating and unparseable sources in a temporary directory. It checks the production task's source coverage without editing or compiling the application's source files. CI runs it when its implementation or PMD wiring changes.

Separate gates prove the facts that the graph needs. `gate:runner-contract` proves how `vp run` executes commands. `gate:lint-contract` proves that every house rule, type-aware diagnostic, and webapp design-system check still fires through `vp lint`. Its positive fixtures use the real Tailwind theme, its imports, and the typography plugin. A misspelled variant proves that the check loaded Tailwind instead of silently using its less precise class grammar.

### Design-system lint

[`@shadcn/lint`](https://github.com/shadcn-ui/lint) runs inside the existing Oxlint pipeline, not a separate ESLint pass. `webapp/.oxlintrc.json` owns the enabled policies and their rationale. `webapp/components.json` owns component and theme discovery. No second alias or token list is necessary.

Inline styles fail unless they forward props, carry CSS custom properties, or document a runtime boundary at the offending line. Padding and gaps use the theme spacing scale. This is not a blanket ban on layout formulas or arbitrary selectors.

Selected indirect vocabulary maps are also checked at their definitions where indirect consumers exceed the plugin's one-hop analysis.

Run `vp run lint:webapp` to check the webapp. Run `vp run gate:lint-contract` to verify enforcement.

The plugin's [analysis limits](https://github.com/shadcn-ui/lint/blob/main/docs/how-it-works.md) matter: a clean lint result does not prove contrast, correct runtime classes or interaction behavior. Review both light and dark themes. Run the affected component tests. A color suggestion is based on proximity in light mode, not on its semantic purpose. Select the existing token for the meaning. A genuine false positive gets a line-level suppression with its reason, never a palette-wide exception.

## Retired paths

Run `node --test scripts/check-no-legacy.test.ts` to test the retired-path check. Run `node scripts/check-no-legacy.ts` to check source deprecations, configuration aliases, path redirects and docs redirect stubs. Provider schemas and provider payload mixins follow their upstream contracts. Authentication, current landing pages and the canonical workspace-host redirect remain current flows. Review also checks behavior that source patterns cannot classify.

`scripts/no-legacy-baseline.json` records the remaining removals during the release candidates. Each entry names its owner and records its source file's fingerprint. A file with an entry stays frozen until its retired paths and entries are removed together. The gate rejects new entries, larger counts, changed fingerprints and stale entries. The first baseline can record only files unchanged from the Git base. Stable 1.0 and later releases require an empty baseline.

## Debt guards

`vp run gate:adr-confirmation` checks every Accepted ADR. Its Confirmation section must name an existing test class, gate, test task or script in code spans. Every named executable reference must resolve. A test method suffix such as `BoundaryTest#checksBoundary` resolves its class. The gate checks existence, not test coverage or method behavior.

Existing gaps live in `scripts/baselines/adr-confirmation.json`. Each entry names its owner, reason and removal condition. New debt fails even when the total count stays the same. The gate also compares the baseline with the base branch and refuses new entries. When a check exists, delete its stale baseline entry. The first PR for a baseline establishes its ceiling.

The gate runs offline and includes its own tests. The affected selections run it for docs, server, webapp, extension and agent changes. Their CI quality legs also run it. Only the reference forms above count as checks. A workflow name alone does not count.

## Repository tooling

Use these conventions when changing tasks or lint configuration.

### Task vocabulary

Task names are lowercase colon-separated words. A word can also contain digits or hyphens. The first word identifies the task type. Only these prefixes are allowed:

| Prefix         | Meaning                                                                                                                        |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `affected`     | Internal groups selected by `check:affected` for one changed tree                                                              |
| `build`        | Produce a distributable artifact                                                                                               |
| `check`        | Run an uncached read-only check or a group of checks. `check` is the local quality entry point.                                |
| `ci`           | Internal groups shaped for CI jobs and runner platforms                                                                        |
| `db`           | Inspect or update the development database schema and its generated documentation                                              |
| `dev`          | Start, stop, reset, or configure local development infrastructure                                                              |
| `docs`         | Build, serve, or lint the documentation site                                                                                   |
| `fix`          | Apply formatting and safe lint fixes                                                                                           |
| `format`       | Apply formatting. A final `check` segment makes the operation read-only.                                                       |
| `gate`         | Produce one verdict that CI can annotate. Every gate belongs to `quality` unless explicitly CI-only in the task contract test. |
| `generate`     | Regenerate a committed artifact from its authoritative source                                                                  |
| `job`          | Run a long command detached from the caller, and wait for it in bounded steps                                                  |
| `lint`         | Run a linter. A final `fix` segment applies safe fixes. `report` writes a report.                                              |
| `pr`           | Follow a pull request on GitHub                                                                                                |
| `quality`      | Internal graph with every local quality gate. Use `check` at the command line.                                                 |
| `release`      | Prepare or publish a release version                                                                                           |
| `report`       | Turn existing results into a human- or machine-readable report                                                                 |
| `schema`       | Refresh a checked-in external integration schema or contract fixture                                                           |
| `sync`         | Reconcile checked-in values with an authoritative local source                                                                 |
| `test`         | Execute a test suite                                                                                                           |
| `typecheck`    | Run a language type checker without emitting artifacts                                                                         |
| `verification` | Run credential-free builds and test suites beyond `quality`. The bare name is the internal graph.                              |
| `verify`       | Run the complete credential-free local verification entry point                                                                |

Segments after the prefix identify the subject and specialization, such as `test:server:integration`. `:webapp`, `:server`, and `:agents` are the tree scopes. `:java` scopes only `format` and `lint`. The Java leg of `check` is `gate:server`.

### Lint and format

Oxlint lints. Oxfmt formats, sorts imports, and sorts Tailwind classes. One rule set has multiple layers. The root `.oxlintrc.json` is the base for every TypeScript file and the config for the Node trees. These trees are the agent runner, the practice script toolkit, `scripts/**`, `load-tests/**`, and the task graph. `oxlint.react.jsonc` is the React layer.

`oxlint.app.jsonc` adds the application compiler, Vitest, and Storybook policy for `webapp/.oxlintrc.json` and `extension/.oxlintrc.json`. Docs extend the base and React layer. Each tree holds only its additions or different decisions, with a reason for each. Write a rule once.

`gate:docs-lint` type-checks the docs tree and runs `markdownlint-cli2` (`docs/.markdownlint-cli2.jsonc`). `docs:lint` is its alias.

* **Run oxlint through `vp` from the repo root**, as CI does:

  * `vp -C webapp lint .`
  * `vp -C extension lint .`
  * `vp -C docs lint .`
  * `vp run gate:agents-lint`

  The `options` block (`typeAware`, `typeCheck`, `reportUnusedDisableDirectives`) travels through `extends`. Thus, a bare `oxlint` started inside a tree sees the same rules. It resolves `tsconfig.json` from where it started.

* **`extends` carries `categories`, `rules`, `plugins`, `overrides`, and `options`.** Each tree owns its `env`, `settings`, and `ignorePatterns`. Vite+ takes the config as an object and accepts only objects in `extends`. Thus, `webapp/tools/oxlint/load-config.ts` inlines the files for both Vite configs. It drops their `jsPlugins`, because that path rejects relative plugin paths. The file on disk keeps the paths for the CLI and editors.

* **Type-aware rules need a file named exactly `tsconfig.json`.** The root stub supplies one for the Node trees configured by `tsconfig.node.json`. `load-tests/tsconfig.json` types the k6 scripts the same way.

* **The house rules are one oxlint plugin under `webapp/tools/oxlint/`.** All tree configs load it. Each config chooses which rules to turn on. Thus, adding a rule to the plugin enables it nowhere. § oxlint traps has the rest.

* **`vp run` does not give a command a POSIX shell on every platform.** Thus, a command never contains `$`. `scripts/ci-contract.test.ts` enforces this. When the config loads, `vite.config.ts` determines what a command reads from the environment. `scripts/check-runner-contract.ts` proves the runner facts that the graph needs.

  An oxfmt pattern with no `/` matches a basename at any depth. A pattern with a `/` is anchored at the working directory. Thus, a root-only pass pairs its patterns with `!*/**`.

### oxlint traps

Each restriction explains itself in its diagnostic at the call site. The following constraints do not appear in diagnostics. The commands assume the webapp. The extension uses the same layers.

* **Suppress with `// oxlint-disable-next-line <rule> -- <why>`**, above the line named by the diagnostic. Spell the rule as the **diagnostic** prints it: `plugin(rule)` becomes `plugin/rule`. A directive that suppresses nothing fails the build. `options.reportUnusedDisableDirectives` is `error`. Thus, the build detects misspelled rules, nonexistent rules, and suppressions whose reason no longer applies. The next case can still hide the original diagnostic.

* **A hooks suppression silences the whole component.** oxlint routes every React-hooks diagnostic through one directive name. `// oxlint-disable-next-line react-hooks/rules-of-hooks` can appear **anywhere in a component body**. It also suppresses `react(set-state-in-effect)` and `react(no-deriving-state-in-effects)` for that whole component. This happens regardless of the line below the directive. Neighboring components still report diagnostics, so the file does not appear disabled.

  The build still fails because it reports the directive as unused. It reports the directive, not the effect, so the diagnostic you intended to silence disappears. The two effect rules report on the `setState` line *inside* the effect, not on `useEffect`. Thus, a directive above the effect never covers them. Fix the effect.

* **The config validator catches three shapes and misses a fourth.** oxlint refuses to start in these cases:

  * `jsPlugins` names a module that it cannot load.
  * `plugins` names an unknown plugin.
  * A rule names a rule that its plugin does not define, including a namespace with no provider.

  It does **not** detect a rule whose plugin is a built-in omitted from `plugins`. It drops that rule with no diagnostic and a zero exit. Check this with one command. A config of `{"plugins":["typescript"],"rules":{"unicorn/prefer-node-protocol":"error"}}` passes a file that imports `"path"`.

* **`react/forbid-dom-props` sees a DOM element and not a component.** `data-testid` on a `<div>` fails the build. The same attribute handed to `<Button>` or `<Textarea>` and spread onto the DOM from inside does not fail. Thus, the ban does not cover the component layer. Do not treat that gap as permission.

* **A nullable string, number or boolean in a condition names its falsy case.** `typescript/strict-boolean-expressions` reports the truthiness check, but not its replacement. Use the applicable check:

  * For a string, `hasText(s)` (`@/lib/text`).
  * For a count, `n !== undefined && n > 0`.
  * For a boolean, `flag === true`.
  * For a `ReactNode` slot, `rendersContent(node)` (`@/lib/react-node`). Here, `0` renders and `false` does not.

  A `ReactNode` slot typed `ReactElement | undefined` needs none of these checks.

* **The one exception `jsx-a11y/no-autofocus` earns** is the first field of an overlay the user just opened. Suppress that case inline with the reason. Everything else is the bug the rule describes.

* **Before you trust or change an off rule's reason, re-derive its reports.** From the repository root, run `vp -C webapp lint -A all -D <rule> --report-unused-disable-directives-severity=off .`. `-A all` excludes the rest of the rule set from the result. The severity flag excludes suppressions that those excluded rules left unused. A nested `overrides` entry still applies. Thus, a rule that an override disables for some files stays off there.

* **An `off` entry pins a decision and carries its reason.** The root `.oxlintrc.json` header owns the base policy. If a category enables the rule, explain why it does not apply here. Otherwise, explain why a rule that someone could select is wrong here. Thus, the decision survives a category change in an oxlint release. If this tree disables a rule enabled by the base, name the different constraint, not a preference. Examples are the browser target and the React Compiler.
