Skip to main content

Local verification

CommandWhat success provesExcludes
vp run check:affectedSelected gates passed, including any tests they containUnselected scopes, verification-only suites and builds, and CI-only checks
vp run checkEvery gate the quality task in vite.config.ts runs passedDistributable builds, browser and live-service tests, images, mutation analysis, and release security
vp run verifyThe local verification suites and builds passedServer 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 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 pathTask
webapp/**affected:webapp
server/** outside the agent and practice script resourcesaffected: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 pathquality

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.

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). 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. 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 moduleRaw bytesGzip bytes
Storybook preview runtime1,186,100339,000
axe-core600,000165,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.

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 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 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 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 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:

PrefixMeaning
affectedInternal groups selected by check:affected for one changed tree
buildProduce a distributable artifact
checkRun an uncached read-only check or a group of checks. check is the local quality entry point.
ciInternal groups shaped for CI jobs and runner platforms
dbInspect or update the development database schema and its generated documentation
devStart, stop, reset, or configure local development infrastructure
docsBuild, serve, or lint the documentation site
fixApply formatting and safe lint fixes
formatApply formatting. A final check segment makes the operation read-only.
gateProduce one verdict that CI can annotate. Every gate belongs to quality unless explicitly CI-only in the task contract test.
generateRegenerate a committed artifact from its authoritative source
lintRun a linter. A final fix segment applies safe fixes. report writes a report.
prFollow a pull request on GitHub
qualityInternal graph with every local quality gate. Use check at the command line.
releasePrepare or publish a release version
reportTurn existing results into a human- or machine-readable report
schemaRefresh a checked-in external integration schema or contract fixture
syncReconcile checked-in values with an authoritative local source
testExecute a test suite
typecheckRun a language type checker without emitting artifacts
verificationRun credential-free builds and test suites beyond quality. The bare name is the internal graph.
verifyRun 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.