Contributing
Build from source
Section titled “Build from source”git clone https://github.com/ddikman/verikun && cd verikunnpm install # dev deps (typescript, @types/node); also builds dist/ via the prepare hooknpm link # optional: put `verikun` and `vk` on your PATHWithout npm link, run it as node dist/bin/verikun.js <command>.
npm run build # tsc: src/ -> dist/npm run dev # tsc --watchnpm test # type-check + run the unit suitenpm run test:watch # re-run the suite on changeNo linter is configured. TypeScript strict is the only static check, so a clean
npm run build (or npx tsc --noEmit) is the static gate.
The unit suite
Section titled “The unit suite”npm test type-checks and runs the suite via Node’s built-in test runner (node:test +
node:assert). No test framework is installed, in keeping with the
zero-runtime-dependency ethos.
Tests live in tests/*.test.ts and compile via tsconfig.test.json into the gitignored
.test-build/, which node --test .test-build/tests/*.test.js then runs.
The *.test.js glob is load-bearing
Section titled “The *.test.js glob is load-bearing”Do not “simplify” it back to a bare directory. It matches only test files, so
tests/helpers.ts compiles alongside them but never runs as a test; a single * does not
cross a directory boundary, so the device suite in tests/e2e/ is type-checked but never
run by npm test (widening it to ** would turn CI red for everyone without a phone
attached); and an explicit file list runs the same way on Node 20 and 22, both of which are in
CI.
The platform-agnostic core — the layers that never touch adb / xcrun / idb, so no
device is needed: args.ts, ui/selector.ts, ui/android-parse.ts, ui/ios-parse.ts,
ui/format.ts, image.ts, report.ts, errors.ts, the whole of device/ (claims.ts,
grant.ts, settings.ts, pool.ts, failover.ts, prep.ts), plus pure helpers from
cli.ts, commands/, run.ts and drivers/adb.ts.
A handful of those helpers — escapeText, evalAssert, parsePoint, healNote, stepName,
rolloverReason, grantLanes — are exported solely so the suite can reach them. Keep
them exported.
The drivers themselves and the getElements → uiautomator round-trip are intentionally
not unit-tested; that is what vk doctor and vk ui against a real device cover.
When you add a pure function to the core, add a tests/<module>.test.ts case. When you add
a platform method, it stays device-verified.
Reporting
Section titled “Reporting”npm test uses the spec reporter. CI runs npm run test:ci, which fans out to two
reporters at once: spec to stdout, and a custom reporter (scripts/github-test-summary.mjs)
to test-results/summary.md, which the workflow appends to $GITHUB_STEP_SUMMARY. That
reporter is plain ESM with zero dependencies, because node --test loads it directly rather
than through tsc. Keep it dependency-free.
The Flutter fixture app (device e2e)
Section titled “The Flutter fixture app (device e2e)”example/flutter-app/ is a small Flutter app whose accessibility semantics we control, and
tests/e2e/ drives it through the built CLI to pin the machine contract on real
hardware. Both example/*.md natural-language tests target the same app, so the prose tests,
the e2e suite and the documented behaviour share one source of truth.
npm run flutter-app:apk # fvm flutter build apk --debugnpm run flutter-app:ios # fvm flutter build ios --simulator --debugnpm run test:e2e # needs a real devicenpm run test:e2eneeds a real device and is NOT part ofnpm test. SetVK_E2E_DEVICE=<serial>for Android, orVK_E2E_PLATFORM=iosfor the simulator. With no device or no fixture installed, the suite skips with a diagnostic rather than failing.ci.ymlmust stay device-free — do not wire this into it.- It is a harness, not
vk batchorvk ai.batchcannot express “this SHOULD exit 2”, andvk aiheals around precisely the failures worth observing.tests/e2e/harness.tsspawnsdist/bin/verikun.jsand asserts on{code, stdout, stderr}. - The app is deliberately stateless. With all state in memory,
vk launch(which force-stops first) is an identical full reset on both platforms, wherepm clearis not portable. Do not add persistence. SemanticsBinding.instance.ensureSemantics()inlib/main.dartis load-bearing — without a heldSemanticsHandle, Flutter emits no semantics tree at all andvk uisees one emptyFlutterView. Never dispose it.- Every widget wraps in
MergeSemantics, so its id, label and state flags land on one node. Give every identified node a label too: one with an identifier but no label survives on Android and vanishes from the iOS tree. example/flutter-app/README.mdholds the measured facts — whatvkactually reports for each widget, per platform. Keep that file measured, not aspirational.@vk_devicereads fromMediaQuery, so a test can assert the app observed a device change rather than only that a value landed. The app’sdarkThemeexists for the same reason: with only a light theme a dark-mode assertion would pass vacuously.
The Flutter SDK is pinned by the committed example/flutter-app/.fvmrc. Always invoke
fvm flutter, never bare flutter.
The documentation site
Section titled “The documentation site”This site is an Astro Starlight project in docs/, with its own package.json and
lockfile. The repo root has no workspaces key, so root npm ci never descends into it.
Run it from the repo root — these wrap the --prefix docs commands so you never have to
cd:
npm run docs:install # once — installs the site's dependencies (needs Node >= 22.12)npm run docs # local preview at http://localhost:4321/verikun/npm run docs:build # production build; fails on a dead internal linkOpen http://localhost:4321/verikun/, not bare localhost:4321 — base: '/verikun' in
astro.config.mjs mirrors the project Pages URL, so the root path is a 404.
Astro 7 needs Node >= 22.12 while the CLI supports Node >= 18 and CI runs 20.x, so your
shell’s Node is probably too old — nvm use 22 first, or the build dies with
Node.js v20.x is not supported by Astro!.
It deploys to GitHub Pages from .github/workflows/pages.yml on every push to main that
touches docs/. Pull requests build the site as a check without deploying.
Two changes oblige a docs edit in the same change, and only two: one that moves or adds a
contract — a command, flag, default, exit code, report format, device set key or platform
capability someone could have scripted against — and one that leaves a sentence here false.
Update SKILL.md, README.md and the owning page; for a false sentence, delete the stale clause
rather than restate it. An ordinary bug fix, where behaviour now matches what these pages already
say, takes a CHANGELOG.md line and nothing else.
These pages state the rule. The reasoning behind it lives in CLAUDE.md and in code comments, so a
page gains prose only when a reader must act on it.
Versioning and changelog
Section titled “Versioning and changelog”The version is declared once, in package.json’s "version".
src/version.ts’s VERSION — the vk --version banner and the plan-cache
COMPILER_FINGERPRINT — is generated from it at build by scripts/gen-version.mjs (the
prebuild script). The same script stamps .claude-plugin/plugin.json. Never hand-edit
either generated file: bump package.json and rebuild. tests/version.test.ts fails if the
committed version.ts has drifted.
Any change that affects behaviour — a command, flag, selector rule, exit code, the report
format, or the vk ai grammar/IR — bumps the version (semver: patch for a fix, minor for
a new capability, major for a broken contract) and adds a CHANGELOG.md entry under
## [Unreleased]. The rebuild rotates COMPILER_FINGERPRINT, so every cached vk ai plan
recompiles against the new build; that is intended.
Releasing to npm
Section titled “Releasing to npm”verikun is published to the public npm registry by pushing a tag;
.github/workflows/publish.yml runs the publish.
Prepare the release in the PR: bump version, run npm run build (regenerates the two
generated files), and move CHANGELOG.md’s ## [Unreleased] heading to
## [X.Y.Z] - YYYY-MM-DD. npm pack --dry-run is worth an eyeball before you tag. Once
merged:
git tag v0.20.0 && git push origin v0.20.0The workflow refuses a tag that disagrees with package.json’s version (npm forbids
republishing, so a mismatch has to fail before the number is burned), re-checks that the
committed generated files are current, verifies package-lock.json is in sync, runs the unit
suite and the packaging check, then publishes with provenance.
A tag carrying a prerelease suffix (v1.0.0-rc.1) goes out under the next dist-tag rather
than latest, so npm install -g verikun is untouched and testers opt in with
npm install -g verikun@next. The GitHub release is marked as a pre-release too, and its
notes come from the section of the release it is a candidate for — v1.0.0-rc.1 uses
## [1.0.0] — unless the rc has a ## [1.0.0-rc.1] section of its own, which then wins.
The GitHub release is a separate job — so if the publish succeeded and only that job failed, do not re-push the tag (the republish would fail). Re-run the job, or create the release by hand.
npm trusted publishing (OIDC). There is no NPM_TOKEN in this repo and nothing to
rotate.
npm’s per-package config names the workflow by filename, so renaming or moving
publish.yml breaks publishing with an auth error at publish time and nothing sooner.
Rename it and the npm side in the same change.
Break-glass: a local npm login plus npm publish still works, until or unless the npm
account turns on “require 2FA and disallow tokens”.
The repo doubles as a Claude Code plugin
Section titled “The repo doubles as a Claude Code plugin”.claude-plugin/marketplace.json and .claude-plugin/plugin.json make this repo installable
as a marketplace plugin. Validate manifest changes with claude plugin validate ..
The plugin ships the skill at .claude/skills/verikun/SKILL.md — referenced via the
manifest’s "skills": "./.claude/skills/verikun/", not moved or duplicated.
skills lists one entry per shipped skill — never the container ./.claude/skills/,
which would publish the contributor-only create-pr skill, and never just one, which would
drop suggest-verikun-improvement even though the main skill hands off to it.
tests/plugin-manifest.test.ts derives the expected set from metadata.internal: true on
disk, so a new skill fails the test until the manifest lists it.
Because dist/ is gitignored, an installed plugin carries the skill but not a runnable
vk; the CLI is a separate npm step.
For what "files" must name and why, see
Contracts.