A validation framework for checking documents and APIs against specifications using OpenLint rulesets. Ships a CLI toolkit, a programmatic engine, an embeddable React UI, and shared build/lint/format/typescript configs so consumer apps stay small.
Release notes are in CHANGELOG.md; see Versioning & releasing for how it is produced.
| App | Description |
|---|---|
| ogc-checker | Validates JSON-FG documents and OGC API endpoints (Features, Processes, Records) |
| don-checker | Validates OpenAPI specs (ADR 2.0.2/2.1.0) and publiccode.yml files |
Each checker app ships its own CLI with baked-in standards and versions:
ogc-checker validate --standard json-fg --input ./data/spec.json
don-checker validate --standard adr --version 2.0.2 --input ./openapi.jsonOr via stdin:
cat spec.json | ogc-checker validate --standard json-fg--version is optional and defaults to the latest final version. The old --ruleset <slug> flag
still works as a deprecated alias — it warns on stderr and resolves the old slug to the same
standard/version.
| Flag | Description | Default |
|---|---|---|
--standard <slug> |
Standard to validate against | (required) |
--version <id> |
Version of the standard | latest final |
--ruleset <slug> |
Deprecated alias for --standard/--version |
— |
--input <file|-> |
Input file, URL, or - for stdin |
- |
--format <fmt> |
Output: table, json |
table |
--fail-on <level> |
Exit code policy: none, warn, error |
error |
Exit codes: 0 = pass, 1 = failed per --fail-on policy, >1 = unexpected error.
Relative $refs are resolved against the document's own location, so a schema next to the input
file resolves no matter which directory you run from:
don-checker validate --standard adr --input media/openapi.json # "$ref": "./ProblemJson.schema.json"The same holds for --input <url> (relative $refs are fetched relative to that URL, following
redirects) and for absolute http(s) $refs anywhere. Stdin has no location, so relative $refs
there resolve against the working directory.
Rules apply to the resolved document, which means a violation can land in a referenced file. Each diagnostic therefore carries both:
| Field | Meaning |
|---|---|
ruleset |
Conformance class that flagged it |
source |
File or URL it lives in — the referenced document, not necessarily the input |
import { mount } from '@geonovum/standards-checker/ui';
import '@geonovum/standards-checker/index.css';
import standards from './standards';
// `standards` is a `Standard[]` — each standard owns an ordered list of versions.
mount(document.getElementById('root')!, standards, {
title: 'My Checker',
});The user picks a version-less standard in the header; a dependent version dropdown then
refreshes and auto-selects the default version (explicit defaultVersion, else the latest final).
Switching version keeps the editor content and only re-lints; switching standard resets the editor
to the new standard's example. The URL anchor is /#/{standard}/{version}; old single-slug links
(and CLI --ruleset slugs) redirect to their new {standard}/{version} anchor.
One package, many subpaths:
| Subpath | What it is |
|---|---|
@geonovum/standards-checker |
Core validation engine, types, and utilities |
@geonovum/standards-checker/ui |
React components, router, mount() |
@geonovum/standards-checker/cli |
createCli toolkit for building a CLI entry point |
@geonovum/standards-checker/vite |
Shared Vite config factory (createConfig) — React + YAML |
@geonovum/standards-checker/vitest |
Shared Vitest config (createVitestConfig + default export) |
@geonovum/standards-checker/vitest-matchers |
Setup file registering the toContainViolation custom matcher |
@geonovum/standards-checker/openlint/core |
Re-export of @openlint/openlint-core (OpenLint, Document, ruleset types) |
@geonovum/standards-checker/openlint/functions |
Re-export of @openlint/openlint-functions |
@geonovum/standards-checker/openlint/parsers |
Re-export of @openlint/openlint-parsers |
@geonovum/standards-checker/openlint/rulesets |
Re-export of @openlint/openlint-rulesets (oas, asyncapi, arazzo, OAS functions) |
@geonovum/standards-checker/spectral/* |
Deprecated alias of openlint/*; removed in the next major |
@geonovum/standards-checker/eslint.config |
Shared ESLint flat config |
@geonovum/standards-checker/prettier |
Shared Prettier config (use via "prettier": field) |
@geonovum/standards-checker/tsconfig.app.json |
Base TS config for app source |
@geonovum/standards-checker/tsconfig.node.json |
Base TS config for Node-side scripts (vite.config.ts) |
@geonovum/standards-checker/client |
*.css and *?raw module declarations for TS |
@geonovum/standards-checker/vitest-client |
Vitest matcher type augmentation (toContainViolation) |
@geonovum/standards-checker/index.css |
Pre-built CSS (Tailwind compiled at build time) |
Bin shipped with the package: build-cli — drives the tsdown JS API with the standard CLI-build
options plus a plugin implementing Vite-style ?raw raw-text imports (rolldown doesn't support the
suffix natively). Usage is build-cli <entry> [...entry]; the build is fully standard — a consumer
tsdown.config.ts is not loaded. vite and vitest are peer deps that live in the consumer's
tree; scripts invoke their native bins directly.
Minimum consumer package.json:
The package ships pre-built CSS (@geonovum/standards-checker/index.css), so consumers don't install Tailwind, PostCSS, or any CSS plugins.
Minimum consumer config files:
// vite.config.ts
import { createConfig } from '@geonovum/standards-checker/vite';
export default createConfig({ base: '/my-checker/' });// eslint.config.js
export { default } from '@geonovum/standards-checker/eslint.config';// tsconfig.app.json
{ "extends": "@geonovum/standards-checker/tsconfig.app.json", "include": ["src"] }// vitest.config.ts
export { default } from '@geonovum/standards-checker/vitest';The shipped Vitest config registers the toContainViolation matcher via its setup file. To override any option, use the createVitestConfig factory:
// vitest.config.ts
import { createVitestConfig } from '@geonovum/standards-checker/vitest';
export default createVitestConfig({ test: { environment: 'jsdom' } });Typical project layout:
my-checker/
├── src/
│ ├── main.ts # Web app entry point (mount(root, standards, ...))
│ ├── cli.ts # CLI entry point (createCli({ name, standards }))
│ └── standards/
│ ├── index.ts # exports the Standard[] — used by BOTH main.ts and cli.ts
│ └── my-standard/
│ ├── standard.ts # Standard definition (name, slug, versions[] with raw rulesets)
│ ├── rulesets/
│ ├── examples/
│ └── functions/
├── tsconfig.app.json
├── tsconfig.node.json
├── eslint.config.js
├── vite.config.ts
├── vitest.config.ts
└── package.json
- Node.js 22.18+ or 24+
- pnpm 10+
pnpm install
pnpm build| Command | Description |
|---|---|
pnpm build |
Type-check + bundle via tsdown + Tailwind CLI (via tsdown onSuccess) |
pnpm dev |
Watch mode (tsdown --watch) |
pnpm test |
Run vitest |
pnpm lint |
Check for lint and formatting issues |
pnpm lint:fix |
Auto-fix lint and formatting issues |
The package is unbundled: each source file becomes its own dist file with bare imports preserved. Vite's resolver follows symlinks' realpath through the installed package, which lets it find this package's own node_modules and resolve transitive deps automatically — no custom Vite plugin needed.
To iterate on standards-checker against a consumer app, use pnpm link:
# Terminal 1: watch mode
cd standards-checker
pnpm dev
# Terminal 2: link the local checkout, then run the checker app
cd ../ogc-checker
pnpm link ../standards-checker
pnpm devUndo with pnpm unlink @geonovum/standards-checker && pnpm install. A subsequent pnpm install in the consumer can overwrite the symlink with a registry install — re-run pnpm link ../standards-checker if that happens.
The peer deps (React family, vite, vitest, esbuild) are already listed in each consumer's package.json because the consumer uses them directly (React imports, vite/vitest/esbuild bins), so peer resolution across the link works without extra steps.
Versioning, the changelog, and npm publishing are driven by Changesets. Describing a change is decoupled from cutting a release:
-
Add a changeset with your change. Run
pnpm changeset, pick the bump (major/minor/patch), and write a one-line summary. This creates a.changeset/<name>.mdfile — commit it with your PR. Omit only for changes that don't affect the published package (CI, internal docs, tests). -
Cut the release. Run
pnpm version-packages(alias forchangeset version). It consumes the pending.changeset/*.mdfiles, bumpspackage.jsonto the computed version (the highest pending bump wins), and prepends the summaries toCHANGELOG.md. Review and commit the result. Don't hand-edit theversionfield — Changesets owns it. -
Publish. Tag the released version and push; the
publish.ymlworkflow verifiespackage.jsonmatches the tag, runs build + lint + tests, and publishes to npm with provenance:git tag v1.2.0 git push --tags
pnpm release(pnpm build && changeset publish) publishes locally if you need to bypass the workflow.
To publish -beta.N prereleases before a final release, enter Changesets pre-release mode first; steps 2–3 above then produce prereleases until you exit:
pnpm changeset pre enter beta # writes .changeset/pre.json — commit itWhile in pre mode:
pnpm version-packagescomputes e.g.1.2.0-beta.0; running it again after new changesets land bumps to-beta.1, and so on. Final releases are blocked until you exit pre mode.- Tag and push as usual (
git tag v1.2.0-beta.0).publish.ymlpublishes any version containing a-under the npm dist-tagbeta, solatestkeeps pointing at the last final release. - Consumed
.changeset/*.mdfiles are kept (tracked in.changeset/pre.json) so the final release's changelog can aggregate them — don't delete them by hand. - The
-beta.Ncounter is derived from the currentpackage.jsonversion — one more reason never to hand-editversion; a hand-set version shifts the counter.
When the release is ready to go final, exit pre mode and version again:
pnpm changeset pre exit # removes .changeset/pre.json
pnpm version-packages # computes the final version and cleans up the consumed changesetsCommit, tag (v1.2.0), and push — the same workflow now publishes under latest.
{ "type": "module", "scripts": { "dev": "vite --open", "build": "tsc -b && pnpm run build:cli && vite build", "build:cli": "build-cli src/cli.ts", "lint": "eslint .", "test": "vitest", }, "prettier": "@geonovum/standards-checker/prettier", "dependencies": { "@geonovum/standards-checker": "^1.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.0.0", }, "devDependencies": { "esbuild": "^0.28.0", "eslint": "^10.0.0", "prettier": "^3.0.0", "typescript": "^6.0.0", "vite": "^8.0.0", "vitest": "^4.0.0", }, }