Semiotic is absolutely slamming PRs right now. The only way that stays useful is if review time is spent on the few things automation cannot see: intent, taste, visual judgment, and the occasional focused code read. The rest has to be boring. Every PR should prove that it did not break the package, the chart catalog, the docs, the AI-facing files, the visual baselines, or the publish artifact before a human has to hold all of that in their head.
Most review time goes here: visual diffs, screenshot updates, and the few rendered changes that actually need human judgment. Code review still happens, but in focused moments instead of as the only safety net.That is the release gate. It is not one heroic CI job. It is a stack of small, specific checks that make drift expensive and hygiene cheap. A PR can add a chart, change a prop, alter docs, touch the MCP surface, or move a visual baseline, but it has to update every dependent surface that now depends on that change.
The PR Shape
The PR starts with the plain checklist: summarize the change, name the tests, say whether the browser was exercised, and call out user-facing changelog work. That template is not the gate, but it gives CI a human companion: what changed, and which part of the gate should matter most.
On GitHub, the main PR workflow runs on Node 22 on Ubuntu. It installs dependencies, builds the package, builds the MCP server, runs the Vitest suite with coverage, and then starts checking all the contracts that are easy to forget when PR volume is high.
Build And Type Contracts
The first layer answers the simplest question: does the thing still build?npm run dist runs the Rollup build and emits the library bundles and declaration files. npm run build:mcp bundles the MCP server. npm run typescript checks the library,npm run typescript:tests checks the test suite against its baseline, and npm run typescript:mcp checks the MCP project.
The declaration files get a second explicit pass later in the job. CI checks that the entry-point .d.ts files exist for the root package and the subpath packages like semiotic/xy,semiotic/network, semiotic/realtime,semiotic/ai, semiotic/server, andsemiotic/themes. A bundle can compile and still be useless if the published types vanish, so that gets its own failure mode.
The Chart Registry Is The Spine
Semiotic has one canonical chart registry, and most hygiene checks are just different ways of asking whether every downstream surface still agrees with it. check:chart-specs verifies the registry, schema, validation, and metadata round trip. If a chart appears inchartSpecs.ts, it needs to be represented where the schema and AI tooling expect it.
From there, check:capabilities verifies thatai/capabilities.json is fresh,check:visual-baseline-capabilities makes sure visual coverage lines up with the catalog, and check:capability-coverage in the local release gate keeps the capability matrix from becoming a hand-wavy marketing file. These are boring names for an important rule: if a chart is real, the agents and docs should know what it can do.
Docs Are A Contract
The docs gates are deliberately mechanical. check:docs-coverageasks whether every specced chart has a docs page or an explicit burn-down exception. check:docs-prop-tables catches prop tables that drift away from chartSpecs.check:docs-playground-controls does the same for playground controls, where a missing control is a quiet product bug.
The blog has its own small gate. Blog entries live in a React registry and a metadata-only registry so build scripts can read post metadata without importing JSX. check:blog-entries keeps those two files in lockstep. Draft posts stay routable for preview but stay out of the index, RSS feed, and SEO prerender metadata until their draft flag is removed.
The full docs build runs after merge through the docs deployment workflow: build production bundles, generate TypeDoc JSON, generatellms.txt, generate SSR gallery and demo GIFs, check blog metadata, render OG cards and RSS, build the Vite website, prerender routes, and smoke-check the generated docs. Then it copies the AI-readable docs into the published site: CLAUDE.md,llms-full.txt, schema.json,api-reference.md, and examples.md.
AI-Facing Drift
Semiotic ships more than charts. It ships a contract for agents that need to choose charts, generate props, inspect capabilities, and call the MCP server. That is why PR CI checks llms.txt freshness,CLAUDE.md component coverage, context7.jsonpackage-subpath freshness, MCP registry cross-references, AI/MCP surface parity, HOC JSDoc example coverage, andai/examples.md coverage and drift.
The local release gate adds check:ai-contracts, which keeps generated agent-visible behavior rules synchronized across the files that agents read. The point is not to make the docs bigger. The point is to stop a PR from changing the library while leaving the machine-readable story behind.
Runtime And Visual Gates
Unit tests catch logic. Semiotic also needs to catch rendered behavior.check:ssr keeps server-rendered and client-rendered behavior aligned. The Playwright job runs after the main testing job, installs Chromium, Firefox, and WebKit, builds the library, and runs the visual and integration suite.
The visual suite uses a missing-snapshot bootstrap script. If a brand-new test has no baseline for a browser and OS combination, CI can write that missing baseline and upload it as an artifact. Existing baselines stay locked. A real screenshot diff still fails. That means the suite can grow without turning every new visual test into an infrastructure negotiation, but established behavior remains protected.
CI also validates the canvas test stub. It compares methods used by production stream renderers with methods stubbed insetupTests.ts. If a renderer starts calling a canvas method the test environment does not stub, the PR fails before the next test suite gets a mysterious canvas crash.
Package And Size Gates
Once the PR has proved source-level correctness, CI rebuilds production bundles with npm run dist:prod. It then checks the bundle-size docs in the README, CLAUDE.md, and the AI system prompt, runs size-limit, and performs a pack smoke test.
The pack smoke test is the one that catches the embarrassing release class: the source builds, but the package a user installs is missing an export, has a bad exports map, or cannot be imported as ESM or CommonJS from a clean consumer project. It packs the current repo into a tarball, installs it in a throwaway project, and verifies every public entry point.
PR CI also runs check:api-surface, which compares the current built declaration surface with the committed API snapshots. That turns a breaking public API change into an explicit diff instead of an accidental side effect of a refactor.
Performance runs separately on pull requests. The benchmark job checks out enough history to compare the PR against origin/main on the same CI hardware. That avoids the common false alarm where a committed baseline from one machine makes every benchmark on another machine look slower. The comparison is not "is this number globally perfect?" It is "did this PR regress relative to main, on the same runner?"
Release Rings
There are two more rings around PR CI. The localnpm run release:check and prepublishOnly scripts chain the same family of gates before a release is cut from a working tree: lint, type checks, MCP build, tests, registry drift checks, docs and AI drift checks, SSR alignment, test-quality baseline, production bundles, bundle sizes, package size, and pack validation.
The tag-triggered release workflow is publish-focused. It rebuilds, retests, checks the core registries, verifies declarations, choosesbeta or latest from the package version, runs the pack smoke test, performs an npm dry run, publishes with provenance, waits for npm propagation, installs the published package in a fresh temp project, verifies the public entry points again, and creates or updates the GitHub Release from the matching CHANGELOG.md section.
What Humans Review
This is why the visual diff loop matters. When the gate is working, human review does not have to be a full re-execution of every invariant in the codebase. It can focus on rendered changes, surprising screenshots, API intent, naming, ergonomics, and the parts of a change where taste or product judgment actually matter.
I still review code. I just try to review it in focused moments: the small files that define a public contract, the helper that multiple chart families now share, the sanitizer that can turn into a security boundary, the release script that can publish the wrong artifact. The rest of the time, the gate makes the obvious mistakes obvious.
The Operating Principle
The operating principle is simple: every source of truth gets a drift check, every generated file gets a freshness check, every public entry point gets an import check, and every visual behavior that matters gets a screenshot. That lets Semiotic absorb a high PR rate without turning the project into a pile of stale docs, stale schemas, stale screenshots, and quietly broken package exports.
The result is not perfect safety. It is a review posture that scales: automation owns hygiene, humans own judgment, and every PR has to pass through both.