# 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`          | 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, run `check`. It validates the contents of all pending changeset files through `gate:changesets`. CI also checks release-note presence and changes against the tested merge base. 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 local cache.

## 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](/contributor/local-development.md#prerequisites) for the pinned toolchain.

## 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` |
| `.github/**`, `.claude/**`, `.agents/**`, `.changeset/**`, `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

`vp install` configures the Vite+ hook dispatcher, which runs the project hooks in `.vite-hooks/`. It preserves a custom `core.hooksPath` and the local preference set by `vp hooks disable`. `vp hooks enable` re-enables the dispatcher. `vp hooks status` shows which hooks are active.

Every worktree of a clone shares `core.hooksPath`. The install sets it to the dispatcher's path relative to the worktree root. Git resolves a relative hooks path against the worktree that runs the hook ([`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath)). So each worktree runs its own dispatcher, its own hooks and its own `vp`. The install removes an absolute dispatcher path, which older Vite+ releases wrote, before it configures the relative one.

Each worktree needs its own `vp install`, which creates that worktree's dispatcher in `.vite-hooks/`. A worktree without one has no dispatcher, so Git runs no hook in it. `vp hooks status` reports the dispatcher as missing. It never falls back to another checkout's `vp` or to one on `PATH`, because that `vp` may not read this worktree's configuration.

The pre-push hook runs `vp run check`. `git push --no-verify` skips it. With custom hooks, run `vp run check` yourself before pushing.

## 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.
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](/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)                   | 600,000   | 165,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](/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.

## 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                                                                  |
| `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.
