# Reproducible backend migration example

From the repository root, use MoonBit with `moonc` 0.10.14 or newer and
Node.js 22 or newer, as required in the [quick start](../../README.md#快速开始):

```sh
moon update
moon check --deny-warn
moon test --deny-warn
moon build --deny-warn
moon run examples/demo
moon run examples/demo-json
node tools/verify-demo.mjs
```

`moon.mod` pins `moonbitlang/regexp@0.3.6` and `walkzzz/re-mbt@0.1.0`.
`../migration-fixtures/fixtures.mbt` supplies every case to the existing
`run_contracts`, adapters, Unicode generator, and report formatters. No mock
engine, parser, matcher, or alternative comparison policy is introduced.

| Suite | Scope | Expected observation |
| --- | --- | --- |
| `shared-ascii` | Both real adapters; seven application contracts | Request-ID match and rejection, search span, unmatched optional capture, empty capture, ordered multiple captures, empty whole match; zero findings |
| `syntax-difference` | Both real adapters; one existing probe | `^([a-z])\1$` on `aa`: regexp matches, re-mbt Perl rejects; one compile-acceptance finding |
| `adapter-domain` | Both adapters; two boundary probes | Literal `猫` pattern is rejected at compile time by the ASCII adapter; ASCII `(a)` on `🦊a` is refused at execution time; two findings |
| `unicode-official-only` | Official adapter; eight cases | BMP literal `猫`, supplementary-prefix span, supplementary literal `🦊`, non-ASCII `Lu` match `Ω`, and four generated boundaries: `A`, `@`, `Z`, `[`; zero findings |

The text output includes every engine outcome, so matching common cases,
non-matches, captures, spans, and rejection reasons are observable.
An unmatched capture is `None`; a participating empty capture is `Some("")`.
An empty whole match is still a match: `^(a*)$` on `""` returns text `""`,
span `[0, 0)` and capture `Some("")` in both backends, rather than `no_match`.
The probe suites omit shared golden expectations: their findings preserve
observations for review without requiring both engines to accept a feature.

`expected.txt` pins all text observations; `expected.json` pins the equivalent
structured output. The JSON envelope has `demo_version: 1` and named `suites`,
each containing the existing report v1 object. It distinguishes unmatched
captures (`null`) from participating empty captures (`""`).

Representative expected observations (the complete output is in `expected.txt`):

| Pattern / input | Backend | Result / captures / UTF-16 span |
| --- | --- | --- |
| `^([A-Z]+)-([0-9]+)$` / `ORD-2048` | Both | `ORD-2048`, `[Some("ORD"), Some("2048")]`, `[0, 8)` |
| `^(a*)$` / empty string | Both | empty text, `[Some("")]`, `[0, 0)` |
| `^([a-z])\1$` / `aa` | regexp | `aa`, `[Some("a")]`, `[0, 2)` |
| same backreference | re-mbt Perl | `compile_rejected`; one compile-acceptance finding |
| `(🦊)` / `猫🦊a` | regexp only | `🦊`, `[Some("🦊")]`, `[1, 3)` |
| `(\p{Lu})` / `éΩ🦊` | regexp only | `Ω`, `[Some("Ω")]`, `[1, 2)` |

Summary lines have `cases/engines/findings` counts of `7/2/0`, `1/2/1`,
`2/2/2`, and `8/1/0`, respectively. The last line requests owner review;
the two intentional difference suites have `compatible=false`.

`node tools/verify-demo.mjs` needs Node.js 22+ and runs both executables, compares
text after newline normalization, and compares parsed JSON without depending
on object key order. Each executable runs twice to check repeatability.
Any unexpected difference fails the regression with a
nonzero exit, even though the demo itself displays intentional findings and
exits normally. The command never rewrites snapshots. To verify another
execution target, pass `js` or `native` as its first argument.

To save observed reports, pass a target and an output directory, for example
`node tools/verify-demo.mjs js artifacts/js`. See [tool instructions](../../tools/README.md)
for diagnostic files and the completion marker. The CI matrix checks default,
JS, and native targets independently, builds packages, enforces warnings,
checks generated interfaces/formatting, and uploads actual demo reports.

When dependencies or fixtures change, inspect both engine outcomes and discuss
any difference with the application owner before editing the expected files.
Pinned dependencies reproduce these particular observations; they are not
a general guarantee about all releases or inputs.

## Apply the example to a migration

1. Record the application-owned pattern/input fixtures, required text/captures
   and UTF-16 spans, baseline/candidate IDs, dependency versions and toolchain.
   The demo's four suites are synthetic examples, not an application's acceptance set.
2. Check that every fixture is inside each adapter's domain. Run shared ASCII
   cases through both bundled adapters; keep official-only Unicode cases separate.
3. Run the commands above and save actual output with
   `node tools/verify-demo.mjs default artifacts/migration-review` (or `js`).
   Inspect every `outcome`, including errors, before reading `compatible`.
4. For each finding, review both sides, the golden requirement, and whether
   the difference is syntax support, an adapter refusal, or observed matching.
   An application owner must explicitly accept any permitted difference.
5. Change application-owned fixtures/expectations only after that decision,
   rerun, and retain the reports with the reviewed commit and decision separately.
   The verifier never updates goldens and does not record human approval.

## Adapter and Unicode limits

`rembt_ascii_perl_adapter` accepts only ASCII **pattern strings and input
strings** (all characters U+0000 through U+007F). This is the first-party
adapter's domain, not a claim that the entire upstream re-mbt project lacks
Unicode capabilities. Non-ASCII patterns return `CompileRejected`; non-ASCII
inputs to a successfully compiled pattern return `ExecutionFailed`. Their
`adapter-domain:` messages explain the guard; they do not measure an upstream
Unicode disagreement. Even an ASCII spelling such as `\p{Lu}` is still
subject to the Perl frontend's syntax support and does not bypass the input
guard.

All observed spans are zero-based, end-exclusive MoonBit `StringView`
**UTF-16 code-unit** offsets. For accepted ASCII, re-mbt's byte offsets equal
these code-unit offsets. On `🦊a`, the official adapter reports `a` at `[2, 3)`:
the supplementary scalar occupies two code units. UTF-8 byte offsets would be
`[4, 5)` and scalar offsets `[1, 2)`. The ASCII adapter refuses this input
instead of performing a speculative conversion.

For the literal search `(🦊)` on `猫🦊a`, the observed `[1, 3)` includes
both code units of the matched supplementary scalar. UTF-8 byte offsets would
be `[3, 7)` and scalar offsets `[1, 2)`. Capture values are text; the adapters
do not expose separate numeric capture-group spans.

The Unicode suite only exercises the official adapter. Its four explicit
fixtures (BMP literal, supplementary prefix, supplementary literal, and Greek
uppercase category search) and first four `Lu` boundary samples are a small regression
sample from the range table generated from Unicode 17.0.0 data, with source
and checksum recorded in the [disclosure](../../docs/2026-10-disclosure.md). The generated four samples happen
to be ASCII; they do not cover all categories, scripts, or supplementary
scalars, prove engine equivalence, or compare re-mbt Unicode matching.

## Human decisions

The application owner chooses required behavior, permitted differences and
whether a migration is acceptable. A normal demo exit displays the report,
including intentional differences; it does not record human approval or
declare either engine wrong. Expected files pin fixture observations rather
than approve migration.
