16 capabilities, grouped by where they sit in the pipeline
Not a checklist of everything the code does — the parts of it a migration actually runs through, in the order it runs through them.
- Hero0.96heroExact match
- Feature grid0.94cardsExact match
- Logo strip0.81logo-cloudVariant
- Comparison table0.42—Needs a new block
- Footer CTA0.98ctaExact match
One model call per cluster — never per region.
The decision the whole product rests on: a model writes the compiler, and the compiler — not the model — processes the pages.
The agent writes the compiler
Cost does not scale with page count. One page or a hundred thousand.
A model migrates one to three exemplar pages interactively. A human approves. The agent then writes a deterministic TypeScript importer module with fixture tests — and that module, not the model, runs over the remaining pages: in parallel, resumable, at zero model cost.
A defect found on page 4,312 is fixed in the importer, so it is fixed for all 10,000 — and the fix is a failing unit test, not a re-roll of a stochastic process.
No model call in the per-page path
Enforced by the dependency graph, not by a policy someone has to remember.
The six packages that do the bulk work — UCM, inference, importer, crawler, emitters, verification — list no dependency by which a model call could be reached. Their entire test suite runs with no network and no credential. A violation would be a package.json change, visible in review.
PARITY, DESIGN_SYSTEM and HYBRID
Pixel-match the old site, or map it into the library you already have.
PARITY reproduces the source. DESIGN_SYSTEM maps every region into an existing Figma-derived library and refuses to invent a block that is not in it. HYBRID lets the two coexist per region, which is what a real migration usually needs.
One component, reused
The same design on two pages becomes one component, not two.
A homepage hero and an about-page banner carrying the same fields are one component with different content — that is what a component is for. A duplicate is refused outright, with the existing name to use instead. The difference is a site where a change is made once and a site where it is made in fourteen places and missed in three.
The verification report shows how many pages use each component. One used on a single page is usually two designs that should have been one.
Start a migration
Pick where the content is coming from.
A live website
Crawl it with real Chromium
A design system
Map into an existing library
A Figma file
Ingest frames as blocks
An existing repo
Start from what is already there
A legacy export
XML, JCR or a content dump
A connected CMS
WordPress, Drupal, Sitecore…
Getting the source in, from a CMS that may not want to cooperate.
Any source CMS
A rendered-HTML crawl works against a site you have no API for.
Headless Chromium captures pages as a browser sees them, so the source does not have to cooperate — no export, no plugin, no vendor migration tool. Native connectors exist for WordPress, AEM 6.x Classic, Drupal, Sitecore, Contentful, Strapi, Sanity, AEM Cloud, SharePoint and legacy exports, and the crawl is the fallback that always works.
Scope, robots and the frontier are all deterministic — the crawler has no path by which a model call could be made.
It reads your CMS too
Structure the CMS can simply state is not guessed at from markup.
WordPress, Drupal, Adobe, Contentful and Sitecore are read through their own APIs alongside the crawl. Anything else gets an API worked out once and then checked against pages already captured before it is trusted. A crawl cannot see content the CMS holds and the page never displayed; a gate now can.
Never required. A CMS with nothing readable migrates exactly as it always did, from the rendered page.
- blocks/hero/hero.js
- blocks/hero/hero.css
- blocks/cards/cards.js
- content/pricing.md
- content/legacy-promo.md
A deleted path keeps its prior content until the next emission, so you can stage the deletion.
export default function decorate(block) {- block.classList.add('hero-legacy');+ const [picture, text] = block.children;+ picture?.classList.add('hero-media');+ text?.classList.add('hero-body'); }Producing the content and the code, for both AEM targets, as a diff you can actually review.
Both AEM targets, co-equal
One block definition emits Edge Delivery and the Universal Editor.
A single BlockType produces the EDS block, its CSS and JS, and the AEM Cloud Service / Universal Editor model, authoring table and component-definition.json. AEM Cloud is not an afterthought bolted on later — the two targets are generated from the same source of truth.
Content and code, not just content
The blocks your migrated content needs to render, generated with it.
Most migration tools move content into components someone else has to build. UEMA generates the blocks, the CSS, the JS and the Universal Editor models — and validates them against the platform contract: no build step, no runtime dependencies, scoped CSS.
The contract is enforced by a validator, not requested in a prompt.
A real diff, and a real pull request
What you review is the commit, not a preview of an intention.
Emission assembles the artifacts and diffs them against your branch. Stage what you want, and UEMA commits, pushes and opens — or reuses — a pull request through UEMA Connect, a dedicated GitHub App rather than a personal access token. A deleted path stays visible with its prior content until the next emission, so the deletion is reviewable too.
Every commit is attributed to the App and its tokens expire in an hour — more on that below.
Your images move too
A migrated site that still serves its images from the old one is not migrated.
Pictures are copied across, converted to a modern format, and stored once however many pages use them. Images served from another company's domain are listed rather than copied, because re-hosting someone else's media is a licensing question nobody asked.
Deduplicated by content, so a logo on every page of a 5,000-page site is fetched once and stored once.
Review queue
What the gates could not clear on their own.
- /pricing/enterpriseStructural sanity
Comparison table matched no block above the confidence floor
Approve Waive with reason - /about/leadershipVisual diff
Visual diff 4.1% — headshot grid reflowed at 768px
Approve Waive with reason - /resources/2019-reportLinks
3 outbound links resolve to 404 on the source site
Approve Waive with reason
Every decision records who made it and why, in an append-only table.
Proving nothing was lost — and handing a human whatever the checks could not settle.
Eight blocking verification gates
A gate, not a report. Nothing ships past a failure.
Content parity, visual diff, links, accessibility, performance, SEO preservation, structural sanity, and — where the source CMS could be read — whether anything it holds failed to reach the page. Each runs deterministically over the emitted output, and a page that fails one does not quietly proceed with a warning attached.
A review queue that records the decision
Who cleared this page, and why — permanently.
Pages the gates could not clear alone land in a queue ranked by what is uncertain about them. Approving or waiving one records the actor and the reason in an append-only table. The decision is the artifact, not the row it removes.
Cockpit
How far the migration has got, and what it has cost.
10,412
Discovered
9,884
Verified
9,102
Published
528
Excluded
$18.40 / $2,500.00
Spend comes from the exemplar passes you asked for, not from page count — the bulk run makes no model call at all.
- Content parity Pass
- Visual diff Pass
- Links Pass
- Accessibility Pass
- Performance Pass
- SEO preservation Pass
- Structural sanity 12 to review
The plumbing that makes this practical to run: providers, tenants, and a way to score a change.
Bring your own model
Thirteen AI providers, one setting — and none of them in the bulk path.
Claude, Gemini, OpenAI, Azure OpenAI, Groq, Mistral, Cerebras, OpenRouter, Together AI, GitHub Models, Hugging Face, Cohere and Cloudflare Workers AI — plus any OpenAI-compatible endpoint, including one running on your own hardware. Use whichever account you already have, switch whenever you like, and route different steps to different providers if a cheap model is good enough for one of them.
Switching provider is a dropdown, not a migration. Adding a credential makes a provider available and reassigns nothing on its own.
Real accounts, tenants and roles
Your migration is yours, and the boundary is tested.
Tenants derive from your email domain. Three roles, a rule that no path can violate — a tenant's last active owner cannot be suspended or removed — and every route naming a project resolves it within your tenant and answers 404 otherwise. An unverified address at a company domain confers no membership at all.
An eval harness for the judgment steps
A change to labelling is scored, not argued about.
The places a model is used — region labelling, block mapping, importer authoring, style import — are scored against a fixed corpus, so a prompt or model change has a number attached rather than a vibe. Adapters exist for the importer, the block mapper and the style importer.
Pushed by an App, not a personal token
Migrated content ships as a pull request, and the credential behind that push is UEMA Connect — a dedicated GitHub App. UEMA never holds its private key; it asks for a token scoped to one installation and gets one that expires in an hour, minted on demand.
contents— write the migrated pages and block code, read the tree to diffpull_requests— every migration arrives as a PR; a human approves before anything reaches a default branch- Commits attributed to the App, not to whoever last pasted in a token
- Access revoked centrally by an organisation administrator, any time
whoever issued the token
the App
long-lived, manually rotated
one hour, minted on demand
only the person who created it
any org administrator
whatever it was given
only what was selected at install
Every one of these has a specification behind it
Ninety-six written increments — requirements, design and tasks — each with one test per acceptance criterion. The design documents record what was rejected, which is usually the interesting half, and they ship with your licence.