@pyreon/atlas — API Reference
Generated from
atlas'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see atlas.
Atlas inverts Storybook’s authoring-first model: your components and their TypeScript types are the source of truth, and Atlas DERIVES the catalog — controls inferred from props, scenarios generated from variant axes (rocketstyle dimensions included), and a five-check verify verdict per scenario (a11y, interaction, leak, reactivity coverage, snapshot). atlas scan writes atlas-catalog.json + atlas-agent-guide.md (the machine-readable surface an AI assistant consumes), atlas dev serves a zero-config workbench over the real Vite compiler, and atlas verify-browser runs the browser half of verification in real Chromium. Authoring is opt-in, not required: an atlas.config.ts can add a theme, a wrapper, presets (viewports / locales / roles), and authored scenarios with play interaction scripts.
Features
Derived catalog: controls inferred from prop types, scenarios generated from variant axes (rocketstyle dimensions resolved through the project theme)
Five-check verify verdict per scenario — a11y, interaction (play scripts or auto click-walk), REAL leak check (reactive-graph accumulation past GC), reactivity coverage, visual snapshot
Machine-readable output: atlas-catalog.json + atlas-agent-guide.md, written for AI assistants as first-class consumers
Zero-config workbench (
atlas dev): real Vite + the real Pyreon compiler, live control editors, canvas addons (viewport/background/zoom/measure/pseudo-states), axe-core a11y panel, autodocs, Actions log, Reactivity LensBrowser verification (
atlas verify-browser): reactive coverage measured on the page’s own devtools bridge + pixelmatch snapshot baselines, merged into the catalogAuthoring opt-in via atlas.config.ts: theme, wrapper, presets (viewports/locales/roles), authored scenarios with step-labelled
playfunctionsHonest verdicts by construction: three states (verified/failing/unverified), red scan = red exit, partial browser coverage named per scenario
Complete example
A full, end-to-end usage of the package:
// atlas.config.ts — ALL of this is optional; scan/dev work with none of it.
// Named exports, plain values, no JSX (the scan imports this file under
// whatever runtime it runs — see the workshop's atlas.config.ts).
export const presets = {
viewports: [
{ id: 'full', label: 'Full', width: null },
{ id: 'kiosk', label: 'Kiosk', width: 900 },
],
locales: [
{ id: 'en', label: 'English' },
{ id: 'ar', label: 'Arabic', dir: 'rtl' as const },
],
roles: [
{ id: 'anonymous', label: 'Anonymous', hint: 'nothing granted' },
{ id: 'ops', label: 'Ops', grants: ['posts.delete'] },
],
}
export const scenarios = {
Button: [
{
name: 'Triple click',
args: { label: 'Storm target' },
play: async ({ root, step }: { root: Element; step: (n: string, r: () => void | Promise<void>) => Promise<void> }) => {
await step('find the button', () => {
if (!root.querySelector('button')) throw new Error('no button')
})
await step('click it three times', () => {
const target = root.querySelector<HTMLButtonElement>('button')!
for (let i = 0; i < 3; i += 1) target.click()
})
},
},
],
}
// A project can also export `theme` (resolves rocketstyle dimension axes)
// and `wrapper` (the providers your components genuinely need to mount).
// Then:
// bunx atlas scan . → atlas-catalog.json + atlas-agent-guide.md
// bunx atlas verify Button → re-check ONE component; says WHICH check failed
// bunx atlas dev . → the workbench at localhost:5210
// bunx atlas verify-browser . → coverage + snapshot verdicts in ChromiumExports
| Symbol | Kind | Summary |
|---|---|---|
atlas scan | function | Discover components (static TS scan + rocketstyle runtime detection), derive controls and variant scenarios, MOUNT each |
atlas verify | function | Re-check ONE component and report WHICH check failed and why — the write → verify → fix loop, for a person or an agent i |
VerifyFinding | type | One thing a verify check found — catalog version: 2. |
atlas dev | function | Boot the workbench: real Vite + the real Pyreon compiler over your source, a derived catalog in the sidebar (nested by d |
atlas build | function | Compile the workbench into a STATIC, deployable site — the same derived catalog atlas dev serves, as plain files for P |
atlas verify-browser | function | The browser half of verification, in real Chromium (playwright-core is an OPTIONAL peer — scan/dev work without it). |
createAtlas | function | The programmatic pipeline factory behind the CLI: discover → decorate → verify → graph, plugin-driven. |
AtlasConfig.projects (monorepo — one site, several packages) | type | Scan several packages into ONE catalog, each filed under its own name (the sidebar reads Core/Forms/Button). |
AtlasConfig.scenarios (authored scenarios + play) | function | Authored scenarios in atlas.config.ts, keyed by component name. |
API
atlas scan function
atlas scan [dir] [--no-mount] [--check]Discover components (static TS scan + rocketstyle runtime detection), derive controls and variant scenarios, MOUNT each scenario (real module load through a Vite-powered loader) and run the node half of the verify pipeline — a11y (static), interaction (mount + play/click-walk), a REAL leak check (reactive-graph accumulation across repeated mounts, past GC), and SSR-PARITY (renderToString + hydrate, asserting the runtime reported no mismatch AND the hydrated DOM equals a fresh client mount — two oracles because SSR and hydrate can agree on the same wrong DOM). Parity skips with a reason when @pyreon/runtime-server is absent, and is blind to typeof window branching because both renders share one process. Writes atlas-catalog.json (every component, control, scenario, and verdict) and atlas-agent-guide.md (the AI-consumable summary). Exits non-zero when any scenario FAILS — wiring the scan into CI gates the catalog. --no-mount keeps the scan purely static (no project code executes). --check turns the scan into a RATCHET: it compares against the COMMITTED atlas-catalog.json instead of rewriting it (a ratchet that overwrites its own baseline compares a run to itself and can never report a regression again) and exits non-zero on a REGRESSION. A check that STOPPED RUNNING counts as one — that is the case absolute counts cannot catch, because losing coverage makes the numbers improve: delete a wrapper and every mount-dependent check drops to skip, so 2 failing becomes 0 failing and a broken catalog reads as fixed. A missing or unreadable baseline is exit 0 with a note, never a failure — making the first --check run red for everybody is how a ratchet gets disabled on day one.
Example
$ atlas scan .
atlas: discovered 10 component(s), 44 scenario(s) — 42 verified, 2 failing, 0 unverified.
checks: a11y 18/20 ✗ · interaction 43/43 · ssrParity 43/43 · leak 43/43
not run: reactivityCoverage, snapshot — browser-only — run `atlas verify-browser`
→ atlas-catalog.json
→ atlas-agent-guide.md
atlas: 2 failing scenario(s):
✗ button--empty
a11y: missing accessible name: "label" is emptyCommon mistakes
Treating "verified" as a default — a scenario is verified only when a check actually RAN and passed;
checked: 0renders as unverified, never smoothed into a passReading
N verifiedas "everything was checked" — it is a scenario count, not a check count. Thechecks:line is the one that says which of the six ran, and a package without@pyreon/runtime-serverresolvable reports 1090/1090 verified having run only two of themRunning the scan without the project theme in
atlas.config.tsfor rocketstyle components — dimension axes resolve empty and the variant scenarios collapse to defaultsExpecting the leak check under plain
node— it needs a GC hook (bun, ornode --expose-gc); without one it reports skip, not passExpecting reactivityCoverage/snapshot verdicts from the scan — those are browser-only claims; run
atlas verify-browserto earn themReading a
--checkrun that reports FEWER failures as an improvement without looking at the ratchet line — fewer failures is exactly what losing a check produces, and only the diff distinguishes "fixed" from "no longer measured"
See also: atlas verify · atlas verify-browser · createAtlas
atlas verify function
atlas verify [Component] [--cwd <dir>] [--json] [--check]Re-check ONE component and report WHICH check failed and why — the write → verify → fix loop, for a person or an agent iterating on a single component. Discovery still walks the whole project (a component’s file is not known until it does), but decoration and verification — mounting, exercising, hydrating and GC-probing every scenario — run only for the match, so this is a question about one component rather than a whole-catalog scan with the answer filtered out at the end. Measured on @pyreon/ui-components (108 components, 1090 scenarios): 1.35s for a full scan against 0.90s scoped to one component’s 60 scenarios; the verify work drops ~18× but discovery dominates the residual, so treat this as a focus tool first and a speed tool second. Prints a per-check tally, the checks that did NOT run and why, and every failing scenario UNCAPPED with its findings. --json emits the same report as data for an agent to branch on. Never writes atlas-catalog.json: a scoped run holds one component, and writing that would replace the whole catalog. Exits non-zero on any failing check, on a name that matched nothing, and on a run where nothing could be verified at all.
Example
$ atlas verify Button
atlas verify Button: 1 component(s), 15 scenario(s)
checks: a11y 14/15 ✗ · interaction 15/15 · ssrParity 15/15 · leak 15/15
not run: reactivityCoverage, snapshot — browser-only — run `atlas verify-browser`
✗ button--empty
a11y [missing-accessible-name]: missing accessible name: "label" is empty
→ Give "label" a non-empty value, or an aria-label if the text is decorative.
1 failing · 14 verified · 0 unverifiedCommon mistakes
Reading exit 0 as "checked and clean" without the
checks:line — a run where nothing could be examined exits NON-zero for exactly this reason, but a run where only two of six checks were available exits 0 and the tally is what says soExpecting a scoped run to refresh
atlas-catalog.json— it deliberately never writes; a one-component catalog would replace the real one and silently break the agent guide, the MCP tools andatlas checkfor every other component until the next full scanPassing a directory as the first positional — the first positional is the COMPONENT (matching
atlas check); the directory is--cwdAssuming a typo degrades gracefully — an unmatched name is a non-zero exit with suggestions, precisely because filtering to nothing otherwise reports "0 scenarios, 0 failing", which reads as a pass
Pattern-matching a finding's MESSAGE instead of its
code— the message is prose and free to be reworded between releases; the code is the stable contract, and each finding also carries afixnaming the one thing to changeExpecting an ambiguous bare name to pick one — a name matching several components across projects REFUSES and names the candidate keys, the same rule the graph and the MCP tools apply
See also: atlas scan · atlas verify-browser · VerifyFinding
VerifyFinding type
interface VerifyFinding { code: FindingCode; message: string; fix?: string }One thing a verify check found — catalog version: 2. code is a STABLE identifier for the CLASS of failure (hydrate-threw, missing-accessible-name, reactive-nodes-retained, ssr-render-threw, reactive-nodes-retained, plus codes for every reason a check did not run: browser-only, no-dom, no-gc-hook, no-ssr-renderer, not-run, nothing-to-check); message is prose; fix names the ONE concrete thing to change, and is present only when there is one — a finding that cannot name a single next step omits it rather than inventing one. The fix travels WITH the finding rather than living in a lookup table a consumer has to know to consult, so the agent guide, the MCP tools and atlas verify --json all carry the actionable half without a second call. Codes are permanent once shipped: a reworded message is a patch, a renamed code is a breaking change. Findings were plain strings at catalog version: 1, which meant an agent could only pattern-match a sentence — the MCP server now refuses a v1 catalog by version rather than rendering blanks for every finding.
Example
{
code: 'missing-accessible-name',
message: 'missing accessible name: "label" is empty',
fix: 'Give "label" a non-empty value, or an aria-label if the text is decorative.',
}Common mistakes
Branching on
message— it is prose, and rewording it is a patch-level change; branch oncodeExpecting every finding to carry a
fix— one is present only when a single concrete next step exists, because a confident wrong instruction costs more than noneReading a
version: 1catalog with code that expects objects — every finding renders blank rather than erroring, which is why the loader refuses by version
See also: atlas verify · atlas scan
atlas dev function
atlas dev [dir] [--port=5210]Boot the workbench: real Vite + the real Pyreon compiler over your source, a derived catalog in the sidebar (nested by directory), live controls (bool/string/number/color editors), canvas addons (viewport / background / zoom / measure overlay / pseudo-state force), an A11y panel with on-demand axe-core, autodocs pages, an Actions log, and the Reactivity Lens. Components in files that import @pyreon/atlas are treated as workbench HOSTS and excluded from the nav (import-specifier match, never substrings).
Example
$ atlas dev . --port=5210
atlas dev: 10 component(s) → http://localhost:5210/Common mistakes
Expecting authored
playfunctions to run on DERIVED catalogs in the workbench — play crosses no JSON boundary; the ▶ button appears for hand catalogs, and derived play scripts run inatlas scan/ the verify pipelineStyling per-instance frames with inline styles — the workbench styles through the Element
cssprop channel (hashed classes); custom viewport widths ship zero inline styles
See also: atlas scan
atlas build function
atlas build [dir] [--out <dir>] [--title <text>] [--base <path>]Compile the workbench into a STATIC, deployable site — the same derived catalog atlas dev serves, as plain files for Pages / Netlify / Cloudflare / S3, with no server component. Crucially it BAKES the two node-answered panels: the Docs source block and the Reactivity Lens read files and run the TypeScript compiler API, neither of which can run in a page, so the build precomputes them per component and ships the answers as data — the Lens still reports real per-expression live/static verdicts on a fully static page. An answer that genuinely cannot be computed bakes its REASON, so the panel says what is wrong instead of surfacing a network error about a request that was never going to work. --out defaults to atlas-dist and a RELATIVE --out resolves against the scanned project, not your shell — atlas build packages/ui --out site writes packages/ui/site, the same base Vite uses for outDir and the same place atlas scan writes its catalog. Pass an absolute path when you want it elsewhere; the resolved directory is always printed. Emits a DIRECTORY PER COMPONENT, so /button/ is a real page on a plain file server — pasteable, bookmarkable, and readable back by the workbench from its own path (the component leaves the query string, so the two can never disagree). Real URLs, not prerendered pages: the body is empty until JS runs. Skipped for a relative --base, whose assets would resolve against the wrong directory. --base is for a subdirectory deploy (--base /my-repo/ for a GitHub Pages project site); --title wins over atlas.config.ts’s title. Fails loudly when discovery finds nothing rather than deploying an empty site.
Example
$ atlas build . --out docs/components --title "Acme DS"
atlas build: 10 component(s) → /repo/docs/components
title: Acme DSCommon mistakes
Assuming a plain
vite buildof the workbench is equivalent — it produces a site that LOOKS complete while the Docs source block and the Reactivity Lens are permanently dark, because nothing baked their node-only answersDeploying to a subdirectory without
--base— assets are requested from the domain root and every one 404s, leaving a blank page with no error on the page itselfExpecting
pages.<name>.titleto rename the component — it is a DISPLAY label only; the usage snippet, the source/Lens lookup and an agent’s import all use the realname, which is exactly why the two are separate fieldsExpecting
pages.<name>.orderto sort across groups — it pins a component within its OWN group; a cross-group sort would scramble the tree from a single config lineExpecting
--dirto apply whenatlas.config.tsdeclaresprojects— the declared roots win, because a monorepo that listed its packages meant it and silently scanningsrcinstead would emit an empty site
See also: atlas dev · atlas scan
atlas verify-browser function
atlas verify-browser [dir] [--update-snapshots]The browser half of verification, in real Chromium (playwright-core is an OPTIONAL peer — scan/dev work without it). Boots the workbench, drives every derived scenario through the workbench model, measures reactive coverage on the page’s own devtools bridge (the components’ actual reactivity instance — a NEW-NODE diff so workbench chrome never pollutes the numbers), screenshots the preview against per-scenario pixelmatch baselines under atlas-snapshots/, and merges both verdicts back into atlas-catalog.json. Coverage is a MEASUREMENT, not a threshold gate: pass means measured, and the findings carry the numbers. First run creates baselines (flagged as recorded-not-yet-compared); later runs compare within tolerance and write <id>.actual.png on a diff. Exits non-zero on visual diffs.
Example
$ atlas verify-browser .
atlas verify-browser: 26 scenario(s) — coverage measured on 26, 0 baseline(s) created, 0 visual diff(s).
→ atlas-catalog.jsonCommon mistakes
Committing
atlas-snapshots/across machines — baselines are machine-specific (font antialiasing); gitignore them and let each environment create its own on first runReading "100% of 0 reactive nodes" as broken — a genuinely static scenario creates no reactive nodes and the finding says so explicitly
Treating not-drivable scenarios as failures — components living in workbench-host files can’t be driven through the dev nav; the summary names them and their browser verdicts stay skip
See also: atlas scan
createAtlas function
(options?: { plugins?: AtlasPlugin[]; preset?: "recommended" | "none" }) => AtlasThe programmatic pipeline factory behind the CLI: discover → decorate → verify → graph, plugin-driven. The recommended preset bundles the built-in plugins (controls inference, variant matrix, mount/interaction/leak verification). Pass preset: "none" when you assemble the plugin list yourself — appending the recommended bundle on top of an explicit list runs duplicate plugins whose default verdicts can overwrite real ones.
Example
import { createAtlas } from '@pyreon/atlas'
const atlas = createAtlas() // recommended preset
const graph = await atlas.build() // discover → decorate → verify → graph
graph.search('button') // Catalog Graph queriesCommon mistakes
Passing an explicit plugin list WITHOUT
preset: "none"— the recommended bundle is appended a second time and a duplicate mount plugin’s empty-graph default verdict can overwrite the real one
See also: atlas scan
AtlasConfig.projects (monorepo — one site, several packages) type
projects?: { name: string; dir: string }[]Scan several packages into ONE catalog, each filed under its own name (the sidebar reads Core/Forms/Button). atlas dev, atlas scan and atlas build all follow it, and --dir is ignored when it is set. The case it exists for: two packages may both export a Button. A component’s IDENTITY becomes project/Name (componentKey), so both survive — in the catalog, in the sidebar, and in their scenario ids (core-button--… vs admin-button--…, which otherwise collide in atlas-catalog.json, in the verify verdicts, and in the snapshot filenames). Its name is untouched, because Button is what you import in both packages and the machine surface an agent reads must say so. Where a bare name becomes ambiguous, Atlas REFUSES and names the candidates rather than picking one. pages and authored scenarios accept either form — 'Core/Button' targets one package, a bare 'Button' applies wherever it is unambiguous (and to BOTH when it is not). A single-package project sets no project, so every derived key, id and group is byte-identical to a scan without this.
Example
export default {
title: 'Acme Design System',
projects: [
{ name: 'Core', dir: 'packages/core/src' },
{ name: 'Admin', dir: 'packages/admin/src' },
],
pages: { 'Admin/Button': { title: 'Button (admin shell)' } },
}Common mistakes
Expecting
graph.get("Button")to return something in a workspace where two packages export one — an ambiguous bare name resolves toundefinedON PURPOSE; ask forCore/Button. Returning the first match is how the original silent-collapse stayed invisibleGiving two projects the same
name— their components would key identically, reintroducing the exact collapseprojectprevents (rejected at config load)Putting a
/in a projectname— it is the key separator, so the resultingA/B/Nameis ambiguous to read and nests a group level the author did not mean (rejected at config load)Keying
pagesorscenariosby a bare shared name and expecting it to hit one package — it applies to EVERY component with that name; use theproject/Nameform to target oneAssuming
--dirstill narrows the scan — declaredprojectsreplace it entirely
See also: atlas build · atlas dev
AtlasConfig.scenarios (authored scenarios + play) function
Record<string, { name: string; args?: Record<string, unknown>; play?: PlayFn }[]>Authored scenarios in atlas.config.ts, keyed by component name. Authored entries are prepended and WIN over generated scenarios with the same id. A play function receives { root, step } — root is the mounted scenario’s container, step(name, run) labels each phase; a throw fails the interaction check naming the exact step. Validated at load: unknown fields error with the correct field name.
Example
scenarios: {
Button: [{
name: 'Submit flow',
args: { label: 'Save' },
play: async ({ root, step }) => {
await step('click', () => root.querySelector('button')!.click())
},
}],
}Common mistakes
Expecting
playto serialize intoatlas-catalog.json— functions never cross the JSON boundary; the catalog records the verdict the play run produced, not the script
See also: atlas scan · atlas dev