Developing Mockingbird
docs/DEVELOPMENT.md · the same file GitHub renders ·Edit on GitHub ·Markdown
Working on this repo: requirements, how the packages are layered, and the quality gates every change passes.
Requirements
- Node.js ≥ 22 or Bun ≥ 1.2 (ESM only).
- npm is required for the package-integrity gates (
bunx publint,bunx attw); Bun runs the rest.
git clone https://github.com/crvouga/mockingbird.git && cd mockingbird
bun run setup # install, build every package, create .env.local from .env.example
bun testThat is the whole onboarding: no secrets, no accounts, nothing self-hosted. bun run setup is idempotent and uses workspaces + Turborepo. Live parity against the real sandboxes needs keys, but they are GitHub Actions secrets: with write access to the repo, bun run parity:remote -- <service> runs it on GitHub without you ever holding one (docs/SECRETS.md). A git hook lints commit messages on the spot — see Quality gates.
Packages
Naming (hard rule): every package is @crvouga/mockingbird-<kebab-case>; the short names below drop that prefix. bun run check:boundaries fails CI on any other name.
Publishing (hard rule): only emulator services (service-<name>) are published. Every other package is "private": true; a service that uses them builds with scripts/bundle-service.ts, which inlines them (JavaScript and .d.ts) so the tarball needs nothing unpublished. bun run check:boundaries fails CI on a public non-service package.
| Layer | Packages | Published |
|---|---|---|
| Services | service-stripe, service-junction, service-genebygene, service-medplum, service-postgres, service-sqlite, and every vendor emulator in the catalog |
yes |
| Core | core (FetchAPI), service (Hono dispatch keyed by operationId) |
bundled |
| Storage | sqlite (SqliteClient port, migrate runner, default @crvouga/mockingbird-service-sqlite) |
bundled |
| Contract | openapi, openapi-metadata, openapi-arbitrary, openapi-codegen |
bundled / build tool |
| Parity | commands, model, canonicalize, parity (runner) |
bundled / tests |
| Adapters | adapter-node, adapter-bun |
tests only |
| Auth | credentials (sandbox credentials for live parity, from the environment) |
tests only |
Infrastructure health
bun run health checks deployed infrastructure and release readiness in one report. This is
separate from the offline quality gate: live checks need network access, gh auth login, and
an npm login to inspect private publisher settings. It never publishes, deploys, repairs
settings, dispatches workflows, or changes credentials.
bun run health # every infrastructure check
bun run health -- npm docs images # selected checks
bun run health -- --verbose # every observation, including optional gaps
bun run health -- --json > health.json # machine-readable report, no log prefixes
bun run health -- trusted-publisher --interactive # permit npm security-key approval
bun run health -- --no-interactive # disable browser approval in a terminal| Check | What it verifies |
|---|---|
npm |
Every public service exists on npm; latest resolves, metadata and tarball integrity exist, current services are not deprecated, exact internal dependency pins exist, stable release tags agree with npm, current tags have GitHub Releases, and retired packages are absent or deprecated |
trusted-publisher |
Each public package trusts crvouga/mockingbird, ci.yml, an empty environment, and direct npm publish; missing packages and unreadable settings are distinct |
github |
Repository and Actions are enabled, main is protected, workflow files are registered and active, latest main runs succeed, schedules are not overdue, and the Actions cache stays below its 10 GiB budget |
secrets |
Required workflow secrets, effective repository/organization secret names, parity workflow env mappings, scheduled parity readiness, and optional local/on-demand credentials; values are never printed or fetched from GitHub |
docs |
Fresh shared hosting contract and authoritative fleet inventory, HTTPS pages and machine-readable routes, all service catalog entries, deployed llms.txt versus main, webhook runtime and read protection, and immutable deployed revision evidence |
images |
Anonymous GHCR pull access, linux/amd64, port 80, and latest matching the immutable SHA tag of the latest successful image publish |
repository |
Local release OIDC permissions, docs publishing contract, every public package's publish configuration, and generated agents/worktree/catalog/README/brand/parity configuration |
Turbo runs seven independent health:<check> root tasks and composes them with
health:all. Every health task has cache: false; --continue=always lets unrelated checks
finish after failures. Each invocation uses an isolated temporary report directory, so old
passes cannot hide crashed, missing, or malformed task output. Debug a task directly with
bun run health:docs, or run selected tasks through Turbo with
bunx turbo run health:npm health:images --continue=always.
Exit 0 means every required check passed. Exit 1 means a failure, pending workflow,
missing access, or another unknown result. The default output lists required problems with
exact package/resource names, reasons, repair commands, and links; --verbose also lists all
healthy and optional observations. JSON includes schemaVersion, checkedAt, healthy, and
the complete per-resource reports. Optional cold parity integrations and local credentials
do not make production infrastructure fail; their missing keys and mappings remain visible
with --verbose or --json.
Terminal runs open npm's security-key approval when needed and display the link while it is
active. Approve it, preferably with its temporary two-factor skip, to inspect all publishers.
JSON, CI, and --no-interactive runs never prompt; publisher health stays unknown if npm
requires approval. Other checks continue independently. Use --interactive to explicitly
enable approval outside a terminal. To repair publisher settings after inspecting the report,
use bun run release:seed -- --trust-only.
The docs build freezes a deterministic source fingerprint into /health.json, independent
of runtime files, secrets, and .git. Health compares it with GitHub's current main source
tree; stale or malformed manifests fail, and an older image without the endpoint remains
unknown until the managed publish workflow deploys it. HTTP availability alone cannot
clear that result. The optional webhook receiver may omit WEBHOOK_READ_TOKEN only when
fleet inventory does not declare it and unauthenticated reads fail closed with HTTP 503.
A declared but unavailable token, or public event reads, fails.
Secret presence does not prove a token is valid or unexpired. Health diagnoses failed
Junction verification artifacts, including vendor HTTP 401/402/403 access blockers, without
printing API response bodies or running billable parity suites. Restore vendor account
access or the named credential, then retry the workflow; access failures never pass as
healthy emulator parity. Run parity:remote for behavioral verification against a sandbox.
bun run secrets:doctor remains a compatibility shortcut to the same npm, publisher, and
secrets checks and accepts the same reporting flags.
Quality gates
Every merge-blocking check is a single command you can run locally. bun run check runs the whole turbo graph; bun run check:full replicates the pull-request gate end-to-end (install + commitlint + check). Passing that gate is the release decision.
CI runs the same turbo graph as bun run check, spread over parallel runners (scripts/ci-plan.ts splits it and fails if the split drifts from the check script): a Static job (format, lint, boundaries, generated-file drift) and four Shard jobs, each running build, typecheck, test, pack and portability for a quarter of the packages. Consumer smoke runs beside them. A Changes job reads Turborepo's dependency graph (scripts/affected.ts) and hands each Shard only the packages a change reaches: each changed package plus everything that depends on it, transitively. A change to one emulator runs that emulator and its dependents, never an unrelated emulator; a change to a shared package runs every dependent. A file that belongs to no package runs everything unless it is on a list of paths the shards do not read (agent docs, worktree config, other workflows), and a bun.lock change reaches only the workspaces whose own entry changed (everything, if an external package or the root moved). Inside the affected packages, turbo's content hashes replay whatever did not change. The required Check job passes when every job the change needed passed.
The GitHub Actions cache is the remote cache (.github/actions/setup); there is no turbo token or server. The CI workflow also runs on every push to main, so main holds a warm turbo cache per job and a PR's first run replays everything it did not change. Each job saves only the task hashes it used, node_modules is cached from main only, and cache-cleanup.yml deletes a PR's caches when it closes and trims main's daily. Locally turbo uses the same cache directory on disk.
Non-blocking checks live in advisory.yml: Biome findings as inline annotations, and live parity (the Parity workflow) for each hot service whose dependency graph the PR changes (see Parity tiers). They depend on live vendors or only restate the blocking lint, so a red advisory check informs the PR without holding its merge.
bun run setup # first time: install + build
bun run check # every gate below, in parallel, cached by turbo
bun run check:full # mirrors .github/workflows/pr.yml (the pull-request gate)| Gate | Command | What it enforces |
|---|---|---|
| Format | bun run check:format |
Biome formatting |
| Lint | bun run lint |
Biome lint (types, style, complexity) |
| Typecheck | bun run typecheck |
tsc for every package. @crvouga/mockingbird-service-conformance proves each HTTP emulator's createRuntime accepts { sqlite?, clock?, seed?, adminKey? } and returns the shared MockSurface |
| Boundaries | bun run check:boundaries |
Intra-workspace dep graph plus the state architecture: internal deps resolve, no cycles or self-deps, imports are declared, published dependency rules hold, and providers cannot bypass or reimplement the shared Timeline history coordinator |
| Package integrity | bun run pack:check |
dist + exports + files, tarball contents, publint, arethetypeswrong (ESM-only consumer resolution) |
| Portability | bun run portability |
Every service emulator's published entry runs in Node, Bun, browsers, and Workers. The check fails when that entry uses a Node- or Bun-only API. A ./server or CLI entry may use Node |
| Generate & OpenAPI | bun run generate / bun run openapi:check |
Regenerate and verify provider contracts |
| Test | bun run test |
Contract, integration, unit, fuzz, and property suites (FC_NUM_RUNS=40 in CI) |
| Consumer docs | bun run pack:check |
Every public package ships a README that opens with the shared epigraph from sites/docs/src/lib/content.ts, then ## Install, ## Usage (a TypeScript example) and ## API listing every runtime export |
| Consumer smoke | bun run release:smoke |
Packs every public package like the release, npm installs the tarballs into a clean project, imports every entry point under Node, and typechecks them plus every README TypeScript example |
| llms.txt | bun run check:llms |
llms.txt lists every published emulator with the parity it declares (bun run llms:sync regenerates) |
| README | bun run check:readme |
README.md is the overview, generated from sites/docs/src/lib/content.ts and these guides (bun run readme:sync regenerates). The catalog of emulators stays on the docs site. Never edit the README by hand |
| Vendor branding | bun run check:brands |
sites/docs/src/data/brands.json has a logo, color and description for every service's mockingbird.vendor (bun run brands:sync fetches them; -- --all --links refreshes all and checks the links). The site serves the same record at /brands.json; admin shells fetch https://mockingbird.chrisvouga.dev/brands.json instead of embedding it |
| Docs site | bun run docs:build (part of build) |
sites/docs renders the same sources, sends every playground sample to a fresh emulator, runs the quick start and SQL snippets, and fails on missing or stale service metadata |
| Agent commands | bun run check:agents |
Every .agents/commands/*.md is symlinked into each agent harness (bun run agents:sync repairs) |
| Parity tiers | bun run check:parity-tiers |
Every service's mockingbird.parityTier is hot, warm or cold (absent means cold) |
| Worktree lifecycle | bun run check:worktree |
Every orchestrator's config (.superset/, .super.engineering/) runs the same scripts/worktree setup, run and teardown (bun run worktree:sync regenerates) |
| Scripts | bun run check:scripts |
Every scripts/**/*.ts typechecks under the base tsconfig (scripts/tsconfig.json), and bun test ./scripts passes |
| Workflows | bun run check:workflows |
Every .github/workflows/*.yml passes a pinned, checksum-verified actionlint (its run: scripts through shellcheck when that is installed, as on GitHub's runners) |
Git hooks (Husky)
commit-msg runs commitlint via bunx for every commit, so Conventional Commits are enforced before they reach a PR. Disable hooks per-repo with HUSKY=0 in package.json scripts, or bypass a single commit with git commit --no-verify (not recommended).
Keep the committed hook file in .husky/commit-msg — the generated .husky/_ shims are gitignored and are produced by the prepare script (husky) on install.
Trunk, checks, and release
main is the only long-lived branch. The ruleset rejects direct pushes and force-pushes, with no
bypass, so every change lands through a pull request. The only requirement to merge is that every
pull-request check has passed:
| Check | What it is |
|---|---|
| Commitlint | Conventional Commits on the PR's commits, and a Conventional Commits title |
| Check | bun run check plus the consumer smoke install (rolls up the Static, Shard and Consumer smoke jobs) |
| GitGuardian Security Checks | Secret scanning |
Advisory checks (advisory.yml) are not required. There is no
required review, no required approval, and an unresolved review thread does not block the merge. A green pull request is releasable: merging it to main publishes. The Release
workflow (.github/workflows/ci.yml) builds (replaying main's turbo
cache) and publishes. It does not re-run the pull-request checks. The branch must be up
to date with main before merge, so those checks ran against the code that lands. Only merge
commits are allowed, because each commit on the pull request is a release input. Head branches
are deleted on merge. PRs use the template in .github/pull_request_template.md.
The gate is codified in scripts/pr-ready.ts (REQUIRED_CHECKS is the ruleset list — rename a
job in .github/workflows/pr.yml and update that list together):
bun run pr:ready repo # verify merge settings / auto-delete / auto-merge
bun run pr:ready repo --apply
bun run pr:ready ruleset # verify the `Protect main` ruleset
bun run pr:ready ruleset --applyAgent commands
Agent commands are written once in .agents/commands/ and symlinked into every
harness — .claude/commands, .cursor/commands, .opencode/command, .windsurf/workflows,
.github/prompts (Copilot), and .agents/skills/<name>/SKILL.md (Codex / Agent Skills). Edit the
canonical file; bun run agents:sync creates missing links and bun run check:agents (part of
bun run check) fails CI on drift.
/pr-ready takes the current branch to a merge-ready PR: commit, push, merge origin/main,
resolve conflicts, open the PR, and fix every failing check (CI and third-party checks such as
GitGuardian). It never merges; a human lands the PR once bun run pr:ready ready reports it green.
For a clean, committed branch, bun run pr:ready advance performs the mechanical steps in one call
and returns JSON for the next blocker. bun run pr:ready comments lists review threads and recent
comments when you want to read them.
/resolve-issues works the queue of GitHub issues that agents in other projects file through
REPORTING_ISSUES.md (label agent-reported): claim the selected issues, confirm
reported behavior against the oracle, add regression tests and fix the emulators. Every invocation
uses one branch, one existing worktree and one PR for the complete batch. Prioritize native
subagents per independent issue or issue group in that same worktree; the coordinator integrates
and runs /pr-ready once, with a Fixes #<n> line for each fully resolved issue. feature requests become acceptance tests plus contract changes;
new-service requests become new packages built through
AUTHORING_A_SERVICE.md.
bun github:resolve-issues [<issue…>] runs that command on GitHub, unattended, as you. It starts
the Resolve issues workflow with a GitHub token minted for
the run, which expires within 8 hours and reaches only this repository, plus your Claude
subscription token. No repo secret is involved. One coordinator opens a single draft PR for the
batch, delegates independent issue groups to native subagents in its shared checkout, and carries
the combined PR to ready-to-merge. Blocked issues are reported individually while independent
work continues. With no issue named, it takes the queue in the command's order. The
repo owner runs bun github:resolve-issues setup once first.
SECRETS.md lists the safeguards.
Package publishing
Published services use publishConfig.access = "public" and publishConfig.provenance = true (npm Trusted Publishing / OIDC). bun run pack:check is the pre-publish gate that confirms each package actually packs, resolves types for an ESM-only consumer, and ships dist.
Docs site hosting
For local development, bun run docs:dev serves the docs and browser examples. Astro uses
separate Vite dependency caches under sites/docs/node_modules/.vite/dev and .vite/build,
so a production build does not replace modules served by an existing dev session. The lab
ordering example's browser dependencies are optimized before its first launch. To verify this
boundary, keep the dev server running, run bun run docs:build, then reload
/examples/medical-testing and open both the Example and Emulators tabs.
The docs site (sites/docs) is hosted on the shared crvouga/workspace fleet as the service
mockingbird-docs (shared-infra contract §4).
sites/docs/Dockerfile builds the static Astro site and serves it
with nginx on port 80. Its build context is the repo root, because the site renders every service
package. On every push to main, .github/workflows/publish.yml
calls the workspace's reusable workflow. That workflow pushes ghcr.io/crvouga/chrisvouga-mockingbird-docs:<sha>,
and then crvouga/workspace deploys that exact image and health-checks it. Railway never builds this repo.
To check the image locally, run docker build -f sites/docs/Dockerfile -t mockingbird-docs . && docker run --rm -p 8080:80 mockingbird-docs.