Quality gates
Every gate uses [quality-gates.<id>] with command (string or argument array), optional
enabled (default true) and optional profiles (default profile when omitted). The command
defines the actual tool invocation. Ward does not install gate tools during check.
| Category | ID | Tool | Typical purpose |
|---|---|---|---|
| Build & Tests | compile | Maven Compiler / Gradle compilation | Compile the project. |
tests | JUnit 5 | Run the project’s automated tests. | |
coverage | JaCoCo | Enforce or report test coverage. | |
mutation | PIT | Run mutation testing. | |
| Code Quality | checkstyle | Checkstyle | Check Java style rules. |
pmd | PMD | Find common Java design and implementation problems. | |
spotbugs | SpotBugs | Run static analysis for potential bugs. | |
sonar | SonarQube | Code smells, bugs, duplication, overall quality. | |
format | Spotless / google-java-format | Check source formatting. | |
| Security | semgrep | Semgrep | SAST and custom security rules. |
gitleaks | Gitleaks | Detect committed secrets. | |
dependency-check | OWASP Dependency-Check | Scan dependencies for known CVEs. | |
zap | OWASP ZAP | DAST against a running application. | |
trivy | Trivy | Vulnerabilities in dependencies, containers, and IaC. | |
| Architecture | archunit | ArchUnit | Run architecture rules/tests. |
enforcer | Maven Enforcer / Gradle checks | Dependency and build rules. | |
| Dependencies & Compliance | license | License Maven Plugin / Gradle License Report (checkLicense) | Dependency license policy. |
api-compat | japicmp | Binary/API compatibility between versions. | |
| REST API contract | openapi-lint | vacuum | Lint the committed OpenAPI spec with every built-in rule. |
openapi-diff | oasdiff | Breaking changes in uncommitted spec edits. | |
openapi-compat | oasdiff | Breaking changes on the branch against origin/main. | |
| TypeScript & JavaScript | tsc | TypeScript compiler | Type-check the project. |
eslint | ESLint | Lint TypeScript and JavaScript sources. | |
prettier | Prettier | Check source formatting. | |
biome | Biome | Lint and check formatting of sources in one run; replaces prettier and eslint where the project uses Biome. | |
vitest / jest / bun-test | Vitest / Jest / Bun test | Run the project’s automated tests. | |
stryker | StrykerJS | Run mutation testing, optionally with a minimum score. | |
dependency-cruiser | dependency-cruiser | Check dependency and architecture rules. | |
| Custom | command | — | Run an arbitrary configured project check. |
| Traceability | openspec-junit-traceability | Ward source analysis | Require one explicitly annotated JUnit mapping per live OpenSpec scenario. |
Choosing gates
Section titled “Choosing gates”ward init offers every gate but selects none unless you choose a preset.
These gates need network access, a running application, or a comparison baseline, so enable them
deliberately, for example in a CI-only profile with profiles = ["ci"]:
zaptrivysonardependency-checkapi-compatsemgrep
Quality presets
Section titled “Quality presets”| Preset | Gates | Minimums |
|---|---|---|
standard | compile, tests, format, Checkstyle, PMD, SpotBugs, coverage | 70% line coverage |
strict | standard plus ArchUnit, Enforcer, and PIT mutation testing | 85% line, 75% branch, 60% mutation |
standard (TypeScript) | tsc, the test runner (vitest, jest or bun-test), prettier and eslint, or biome in a Biome project | 70% line coverage |
standard (JavaScript) | the test runner (vitest, jest or bun-test), prettier and eslint, or biome in a Biome project | 70% line coverage |
strict (TypeScript) | standard plus stryker, with typescript-eslint strict-type-checked | 85% line, 75% branch, 60% mutation |
strict (JavaScript) | standard plus stryker | 85% line, 75% branch, 60% mutation |
For TypeScript and JavaScript, presets write no rule files under config/. Init adds the tools to
package.json and writes the tools’ own configs when the project has none: an ESLint flat config,
vitest.config.mjs or jest.config.mjs with the coverage thresholds, and stryker.config.mjs.
Bun checks lines only, because it does not measure branch coverage. See
Node projects.
Strict mutation testing can add a lot to check runtime. When license is selected, strict
writes a license allowlist instead of the GPL/AGPL denylist; see
what init changes in your build.
Presets may write these rule files:
config/pmd-ruleset.xmlconfig/checkstyle.xmlconfig/spotbugs-exclude.xml(standard)config/enforcer-rules.xml
The files carry a managed-by: ward marker. Unmarked user files are never overwritten.
Build plugins
Section titled “Build plugins”For Maven-backed gates, init picks a version from its embedded Java/Maven compatibility catalog
before adding a missing plugin to pom.xml.
- Existing compatible versions are kept.
- Incompatible versions need confirmation, or
--yesfor explicit automation. - Unknown compatibility, or no compatible version in the catalog, stops init before it writes anything and reports how to fix it.
For Gradle projects, the same gate IDs map to Gradle tasks and pinned plugin declarations, in either the Groovy or the Kotlin build script. See what init changes.
Standalone tools
Section titled “Standalone tools”semgrep, gitleaks, trivy, and zap-baseline.py must already be on the path.
- A missing tool is reported as unavailable with an install link, and other gates still run.
ward initcan install supported tools with your consent;checknever installs them.- When a command contains
--report-path <path>inside the project, Ward creates the parent directory before running it. The command itself never changes.
TypeScript and JavaScript gates
Section titled “TypeScript and JavaScript gates”tsc, eslint, prettier, biome, vitest, jest, bun-test, stryker, and dependency-cruiser
run their command from the project root, exactly as written, and pass when it exits with code 0.
- Ward does not add flags and does not choose a package manager. Put the runner in the command,
such as
pnpm exec,npx,yarnorbunx, so the project’s locally installed tool runs. - For a TypeScript project,
ward initofferstsc,eslint,prettier, the project’s test runner, andsemgrepwith thep/typescriptruleset, and writes their commands with the package manager’s runner; see Node projects. - For a Vue, Svelte, or Astro project, the type-check gate is
vue-tsc,svelte-check, orastro-checkinstead oftsc, because only the framework’s checker reads its component files. The framework comes from the project’s declared packages; see Node projects. Angular’s type-check gate isngc, which checks its templates, and its test gate isng-test. A type-check configuration with no inputs fails the gate and names the file. - For a Vue, Svelte, or Astro project,
strykermutates onlysrc, and the generated ESLint config ignores.astro/(Astro) or.svelte-kit/(Svelte). - For a JavaScript project, it offers the same gates without
tsc, andsemgrepwith thep/javascriptruleset. - In a project that uses Biome, init offers
biomein place ofprettier, and offerseslintonly with the project’s own ESLint config. Thebiomecommand is<runner> biome ci --colors=off --reporter=checkstyle .; understrictit adds--error-on-warnings. See Node projects. - Init also offers
stryker, in theciprofile, andstrictselects it. It offersdependency-cruisertoo, whichstrictselects; see Node projects.
[quality-gates.eslint]command = ["pnpm", "exec", "eslint", ".", "--format", "json"]profiles = ["default"]
[quality-gates.tsc]command = ["npx", "tsc", "--noEmit", "--pretty", "false"]profiles = ["pre-push"]Stryker mutation score
Section titled “Stryker mutation score”Without minimum, the stryker gate passes or fails on Stryker’s exit code alone. With
minimum = <0-100>, Ward also reads Stryker’s JSON report after a successful run and fails the gate
when the mutation score is below the minimum.
[quality-gates.stryker]command = ["npx", "stryker", "run", "--reporters", "clear-text,progress,json"]minimum = 60profiles = ["ci"]- The score is detected / valid × 100. Detected mutants were killed or timed out. Valid mutants are the detected ones plus those that survived or that no test covers. Ignored, pending, compile-error, and runtime-error mutants do not count.
- The command must enable Stryker’s
jsonreporter. Ward reads the report at Stryker’s default path,reports/mutation/mutation.json. - A report that is missing, older than the gate run, unreadable, or without valid mutants is an execution error, never a pass. The message says which case applies.
- Stryker’s own
thresholds.breakalso fails the gate, through Stryker’s exit code. Thestryker.config.mjsthat init writes withstrictusesbreak: 60and sets nominimum, so the threshold lives in one place. Tune it there. Useminimumwhen you keep your own Stryker config and want Ward to report the score.
PIT mutation score
Section titled “PIT mutation score”The mutation gate accepts the same minimum. Without it, the gate passes or fails on PIT’s exit
code, which already enforces any mutationThreshold in your build. With it, Ward also reads PIT’s
XML report after a successful run and fails the gate when the score is below the minimum.
[quality-gates.mutation]command = ["./mvnw", "-DoutputFormats=XML,HTML", "pitest:mutationCoverage"]minimum = 60profiles = ["ci"]- The score is PIT’s own: detected mutations divided by all mutations. It matches what
mutationThresholdwould check. - Ward reads
target/pit-reports/mutations.xmlfor Maven andbuild/reports/pitest/mutations.xmlfor Gradle. PIT writes only HTML by default, so enable XML output:-DoutputFormats=XML,HTMLfor Maven, oroutputFormats = ['XML', 'HTML']in the Gradlepitestblock. Timestamped report folders are not read. - A report that is missing, older than the gate run, unreadable, or without mutations is an execution error, never a pass.
Traceability gate
Section titled “Traceability gate”openspec-junit-traceability is the one built-in gate that needs no command:
[quality-gates.openspec-junit-traceability]enabled = trueEach mapping must be declared directly on a @Test, @ParameterizedTest, or @RepeatedTest
method, using matching exact identities:
@Tag("spec:identity/user-auth")@DisplayName("Successful sign-in")@Testvoid signsIn() {}- Only the existence of a mapping is checked, not test execution or assertions.
- Empty live specs fail with
no scenarios found. - Active-change specs are excluded.
API compatibility
Section titled “API compatibility”api-compat is for published libraries. It compares the latest released artifact (RELEASE) with
the jar the current build produces, so a released baseline must be resolvable from a Maven
repository.
On Maven, init:
- runs
-DskipTests package japicmp:cmp; - points japicmp’s new version at
${project.build.directory}/${project.build.finalName}.jar, so a customfinalNameworks; - limits the comparison to
${project.groupId}.*, so classes bundled from dependencies are not reported; - offers the gate only when
pom.xmldeclares<distributionManagement>.
Applications shipped as binaries, such as CLIs, have no Java API consumers and should not enable it.
OpenAPI contract gates
Section titled “OpenAPI contract gates”openapi-lint, openapi-diff, and openapi-compat check a committed (contract-first) OpenAPI
spec. Each one needs a spec option with the project-relative path of that file. Ward appends the
spec to the command, so the command names only the tool and its flags:
[quality-gates.openapi-lint]profiles = ["default", "pre-commit", "pre-push", "ci"]command = ["vacuum", "lint", "-z", "-n", "warn"]spec = "src/main/resources/openapi.yml"
[quality-gates.openapi-diff]profiles = ["default", "pre-commit"]command = ["oasdiff", "breaking", "--fail-on", "ERR"]spec = "src/main/resources/openapi.yml"
[quality-gates.openapi-compat]profiles = ["pre-push", "ci"]command = ["oasdiff", "breaking", "--fail-on", "ERR"]spec = "src/main/resources/openapi.yml"base = "origin/main" # the defaultvacuum and oasdiff must be on the path; a missing tool makes the gate unavailable. The flags
and behavior below were verified with vacuum 0.35.1 and oasdiff 1.34.1.
Baselines
Section titled “Baselines”| Gate | Compares the spec with |
|---|---|
openapi-diff | The spec at HEAD, so it catches a breaking edit before you commit it. In pre-push and ci everything is committed, so it would compare the file with itself; use openapi-compat there. |
openapi-compat | The merge base of HEAD and base, so only the changes on your branch count. When HEAD is that merge base, as on main after a merge, it compares with the first parent of HEAD. |
A spec that does not exist at the baseline is new: the gate passes and its summary says there was no baseline.
Ward never fetches:
- Fetch the base branch before pushing.
- Give CI a checkout with the base branch history, for example
actions/checkoutwithfetch-depth: 0. Without a merge base,openapi-compatfails and says so. - A
baseref that does not exist is a configuration error (exit2).
No exceptions
Section titled “No exceptions”These gates accept no way to silence a rule, in any profile:
openapi-lintrequires-z(every built-in rule, including OWASP) and-nwithwarn,info, orhint. Besides those it accepts only output flags:-b,-q,-d,-s,-a,-x,-m,-tand their long forms,--no-clip,--abs-paths,--pipeline-output,--github-annotations,--no-update-check, and--min-score. Rulesets (-r), ignore files, and--configare rejected, because a ruleset can lower a rule’s severity even in hard mode.openapi-diffandopenapi-compatrequire--fail-on ERRor--fail-on WARN, and accept only--format,--color,--lang,--flatten-allof,--flatten-params, and--include-path-params. Ignore files, path and extension filters,--config,--open(which uploads the comparison), and--fetchare rejected.
Any other flag, a repeated flag, or a spec path in the command is a configuration error (exit 2)
that names the argument.
Before running, the gates also fail when:
- the project root contains a file the tool loads on its own:
vacuum.conf.yaml,.oasdiff.yaml,.oasdiff.yml, or.oasdiff.json; OASDIFF_CONFIGis set;- the spec contains
x-lint-ignore.
Dependency scanning credentials
Section titled “Dependency scanning credentials”The Maven and Gradle dependency-check configurations read the NVD API key from the NVD_API_KEY
environment variable and never write it to a build file. Without the variable, scans may be very slow
or rate-limited. Request a key from the NVD API service.
Sonatype OSS Index needs credentials, so init turns its analyzer off:
<ossindexAnalyzerEnabled>false on Maven, analyzers { ossIndex { enabled = false } } on Gradle.
To turn it on, set OSS_INDEX_PASSWORD (and OSS_INDEX_USERNAME if your token needs one) in the
environment and flip the setting:
- Gradle reads the variables directly.
- Maven reads credentials from a
settings.xmlserver whose id matches the generatedossIndexServerId(ossindex). Reference the variables there instead of writing values to a file in the project:
<server> <id>ossindex</id> <username>${env.OSS_INDEX_USERNAME}</username> <password>${env.OSS_INDEX_PASSWORD}</password></server>If you do not want to manage NVD or OSS Index credentials, use the trivy gate. It also scans Maven
and Gradle dependencies for known vulnerabilities and needs no API key.
ZAP target
Section titled “ZAP target”zap requires an explicit target in its command. Init writes <TARGET_URL> as a placeholder.
Replace it with an authorized target before running check, for example
command = ["zap-baseline.py", "-t", "http://localhost:8080"].
An enabled zap gate without -t <url>, or still containing <TARGET_URL>, is a configuration
error (exit 2). Ward never infers, starts, or scans a target on its own.
Structured findings
Section titled “Structured findings”When checkstyle, pmd, gitleaks, compile, format, eslint, biome, or tsc fails, Ward turns the tool’s output
into findings with rule, severity, file, line, column, message, and a rule documentation link when
known. They appear in JSON and SARIF output.
| Gate | Source | Maven | Gradle | TypeScript & JavaScript |
|---|---|---|---|---|
checkstyle | XML report | target/checkstyle-result.xml | build/reports/checkstyle/*.xml | |
pmd | XML report | target/pmd.xml | build/reports/pmd/*.xml | |
gitleaks | SARIF report | target/gitleaks.sarif | build/gitleaks.sarif | .ward/reports/gitleaks.sarif |
compile | javac messages in the output | |||
format | Spotless file list in the output (file only, no line) | |||
eslint | ESLint JSON in the output; the command must pass --format json | |||
biome | Checkstyle XML on standard output; the command must pass --reporter=checkstyle. Lint rules keep Biome’s category, such as lint/suspicious/noExplicitAny, and format findings have no line. | |||
tsc | Compiler diagnostics in the output; the command must pass --pretty false |
- Only default report locations are read, and only reports written during the current run. An older report is ignored.
- TypeScript and JavaScript projects keep the reports Ward reads under
.ward/reports/instead oftarget/orbuild/. Stryker’s mutation report is the exception: it stays at Stryker’s ownreports/mutation/mutation.json, because only Stryker’s configuration file can move it. - A missing or malformed report never changes the gate result; the gate just has no findings.
- Gitleaks findings never contain the matched secret.
- ESLint’s default
stylishoutput andtsc’s pretty output give no findings. The gate result does not change. An ESLint parsing error is reported with the ruleeslint. - Other gates have no findings yet.
Partial checks
Section titled “Partial checks”ward init writes a [quality-gates.<id>.partial] table
for checkstyle, pmd, format, semgrep, gitleaks, eslint, prettier, biome,
openapi-lint, and openapi-diff. After an agent
edit, and for the files a commit stages, a gate:
- is skipped when no edited file is in its
inputs; - runs in full when one of its
configfiles was edited; - is narrowed to the edited files when the tool supports it:
| Gate | Maven | Gradle |
|---|---|---|
checkstyle | -Dcheckstyle.includes=**/<file name> | -PwardPaths=<paths>, with a preset |
pmd | full, with PMD’s analysis cache | -PwardPaths=<paths>, with a preset |
format | -DspotlessFiles=.*/<path>, a regular expression matched against the absolute path | full |
semgrep | each path as its own argument | each path as its own argument |
gitleaks | --no-git --source=<path> for each path, so uncommitted edits are scanned | same as Maven |
openapi-lint, openapi-diff | full, only when the spec is edited | full, only when the spec is edited |
In a TypeScript or JavaScript project, eslint and prettier take edited .ts, .tsx, .mts, .cts, .js,
.jsx, .mjs, and .cjs files, each as its own argument. Their tables set command to the full
command without its . target, so a narrowed check runs, for example,
npx prettier --check src/app.ts. An edit of their configs or package.json runs them in full.
biome takes the same form: a narrowed run passes --no-errors-on-unmatched, so an edited file that
Biome ignores passes. tsc and the test runners have no table, so they run in full in pre-push
and ci.
Gradle notes:
- With a preset, the Checkstyle and PMD blocks init writes filter their sources to
wardPathsonly when the property is set, socheckstyleMainandpmdMainstill check the wholemainsource set otherwise. - Without a preset there is no filter, and those gates run in full.
- Gradle’s single-file Spotless mode applies formatting instead of checking it, so Gradle
formatruns in full.
The forms above were verified against the real tools: a path format a tool does not understand analyzes no files and passes. The full check at the end of an agent turn, Git hooks, and CI always check the whole project.
OpenSpec tool validations
Section titled “OpenSpec tool validations”OpenSpec is an optional project tool, not a quality-gate menu entry. A configuration may register the two validations independently:
[quality-gates.openspec]enabled = truecommand = ["openspec", "validate", "--all", "--strict", "--no-interactive"]
[quality-gates.openspec-archived]enabled = truecommand = ["openspec", "validate", "--archived", "--no-interactive"]- The configured executable runs directly from the target root. Relative and absolute paths are honored, with no PATH fallback.
- Both validations need an
openspec/directory at the project root. - A missing executable is reported as
UNAVAILABLE. A missing or non-directoryopenspec/is aFAILEDresult. - Check and doctor never install or scaffold OpenSpec.
OpenSpec in a monorepo
Section titled “OpenSpec in a monorepo”In a monorepo, openspec/ often lives at the repository root while the Java project sits in a
subdirectory. Set root on each OpenSpec gate to the directory that holds openspec/, relative to
the project root:
[quality-gates.openspec]enabled = truecommand = ["openspec", "validate", "--all", "--strict", "--no-interactive"]root = "../.."- The gate then requires
openspec/in that directory and runs the command from it. - Ward never searches other parents. If
roothas noopenspec/, the gate fails and names the configured root. rootmust be relative and stay inside the Git repository. An absolute path is a configuration error, and a path that leaves the repository fails the gate without running it.ward initwritesrootfor a nested project without its ownopenspec/when the repository root has one. Doctor reports the prerequisites of the configured root.- Targeted
ward spec validate -- <paths>uses the same root, and the approved-artifact guard protects that repository-levelopenspec/too.
Legacy identifiers
Section titled “Legacy identifiers”The legacy identifiers architecture (use archunit) and compliance (use enforcer; license
checks now use license) are still read, so existing configurations need no change.
Ward validates registry identifiers; the configured command is the gate-specific part and must be supplied. See configuration syntax.