diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c1fd6ba --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,109 @@ +# The gate: npm test on Node 22 and 24 - schemas against the recorded +# traffic (shipped validator and ajv), docs links and anchors, the portable +# profile, the conformance apps' abapGit format, the CLI, and the backend +# suite against BOTH reference backends: +# node-runtime @abap2ui5/node-runtime + the ABAP apps, transpiled in the job +# (open-abap-core fetched with git, cached by its commit) +# cap2ui5 a CAP project + @cap2ui5/cds-plugin + the JS apps +# plus abaplint over the ABAP apps. Each backend run takes seconds; the +# install dominates. +# The frontend job runs the frontend suite against the UI5 SPA of abap2UI5 +# (the commit the spec is derived from, app/webapp only) in Chromium - the +# test job skips that test, it has no browser and no checkout - and records +# the reports of the UI5 SPA and the agent client. +name: ci + +on: + push: + branches: [ main ] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + node: [ '22', '24' ] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ matrix.node }} + cache: npm + cache-dependency-path: | + package-lock.json + conformance/hosts/cap2ui5/package-lock.json + - run: npm ci + - name: open-abap-core commit of the runtime + id: core-sha + run: echo "sha=$(node -p "require('@abap2ui5/node-runtime/package.json').abap2ui5.openAbapCore")" >> "$GITHUB_OUTPUT" + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .cache/open-abap-core + key: open-abap-core-${{ steps.core-sha.outputs.sha }} + - name: cap2UI5 host + run: npm ci --prefix conformance/hosts/cap2ui5 --no-audit --no-fund + - name: abaplint (ABAP conformance apps) + run: npm run lint:abap + - name: npm test (both backends) + run: npm test + - name: backend suite report - node-runtime + if: ${{ !cancelled() }} + run: node scripts/run-conformance.mjs node-runtime --json report-node-runtime.json + - name: backend suite report - cap2ui5 + if: ${{ !cancelled() }} + run: node scripts/run-conformance.mjs cap2ui5 --json report-cap2ui5.json + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() && matrix.node == '22' }} + with: + name: conformance-reports + path: report-*.json + if-no-files-found: ignore + + frontend: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: abap2UI5 app/webapp (the frontend under test) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: abap2UI5/abap2UI5 + ref: 5d7e91f904bf37b282e8e1c0ecb2b3eeae4c5842 # main; app/webapp as at b812079 + path: deps/abap2UI5 + sparse-checkout: app/webapp + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + cache: npm + - run: npm ci + - name: Chromium for playwright-core + run: npx playwright-core install --with-deps chromium + - name: frontend suite - UI5 SPA and agent client + env: + PROTOCOL_REQUIRE_BROWSER: '1' + PROTOCOL_FRONTEND_REPORT: report-frontend-ui5.json + run: node --test test/frontend.test.mjs + - name: frontend suite report - agent client + if: ${{ !cancelled() }} + run: node conformance/backend/bin/abap2ui5-conformance.mjs frontend --adapter agent --json > report-frontend-agent.json || true + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() }} + with: + name: frontend-reports + path: report-frontend-*.json + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96eedb8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.build/ +.cache/ +deps/ +conformance/apps/deps/ +conformance/hosts/cap2ui5/node_modules/ +conformance/hosts/cap2ui5/srv/apps/ +*.log diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3c62973 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,96 @@ +# AGENTS.md — abap2UI5 protocol + +Guidance for agents and contributors. Read before making any change. + +## What this is + +The specification of the abap2UI5 roundtrip protocol (`spec/`), its view +profiles (`profiles/`), JSON Schemas (`schema/`) and conformance suites +(`conformance/`), plus recorded real traffic (`traffic/`). It is +**descriptive first**: the protocol was implemented before it was written +down, and the spec records what the implementations do. + +## Rules + +- **Every normative statement cites its source.** A MUST/SHOULD names the + file and method it comes from, with the abbreviations of + `spec/README.md#sources`. A statement you cannot trace to code is a + proposal - say so, or leave it out. +- **Accidental behaviour is an implementation note, not a rule.** When the + reference does something odd (draft ids that repeat after a leave, the URL + reflected into a 500 body), write it under *Implementation note* - another + implementation must not have to copy it. +- **A rule a backend can break gets a check.** New MUST/SHOULD for backends -> + a check in `conformance/backend/lib/checks/`, its id on the spec's + *Checked by* line, `node scripts/gen-check-list.mjs`. `npm test` fails when + a check cites a missing section or the check list is stale. +- **A rule a frontend can break gets a frontend check** - in + `conformance/frontend/lib/checks/`, its id on the spec's *Frontend + check(s)* line, the list regenerated the same way. A check scripts the + backend (`t.mock.reply(...)`, recorded responses from `traffic/` first, + synthetic ones for edge cases), drives the frontend only through the + adapter interface (`fill`, `press`, ... with targets every kind of + frontend can resolve) and reads the normalized `state()`. A check that + needs more than the core (a URL, timers, a DOM) declares it in `needs`. +- **A deviation of a real frontend is a finding, not a test to bend.** Decide + whether the spec or the frontend is wrong, write it into + `conformance/RESULTS.md` (and the spec, with its source, when the spec + was wrong), pin the result in `test/frontend.test.mjs`, file it upstream. +- **Provisional decisions go to `spec/open-questions.md`** with the current + decision and the alternatives, so the maintainer can decide. +- **Behaviour the suite needs lives in a conformance app, twice.** A new app + or event goes into `conformance/apps/README.md`, the ABAP class + (`conformance/apps/abap`, abapGit format, abaplint-clean: + `npm run lint:abap`; also clean in the abap2UI5 linter) AND the cap2UI5 + twin (`conformance/apps/cap2ui5`). `test/traffic.test.mjs` requires both + backends to answer every check identically. +- **Traffic is recorded, never hand-edited.** `npm run record` rewrites + `traffic/`; record again after changing a check or an app. +- **Generated sections are generated.** `conformance/backend/README.md` + (check list), `profiles/portable.md` (between the `portable:*` markers, + from `profiles/portable-v1.json`) - run `npm run generate`. +- **`profiles/semantic.md` is the normative snapshot v1** (moved from + abap2UI5/mcp-server `docs/agent-snapshot.md`). Changes to the snapshot + shape are made here first; implementations follow. +- **The protocol number moves only with a breaking change** + (`spec/versioning.md`). Additive changes keep protocol 2. +- **Do not edit other repositories from here.** A difference found in a + backend is filed in `conformance/RESULTS.md` (and upstream), not patched. + +## Layout + +| Path | | +|---|---| +| `spec/` | the core protocol, one file per area | +| `profiles/` | UI5, portable (v1 + `portable-v1.json` + coverage) and semantic profiles | +| `schema/` | JSON Schemas 2020-12 | +| `conformance/backend/` | the backend suite - the published package's entry (`index.mjs`, `bin/`) | +| `conformance/frontend/` | the frontend suite - `lib/` (scripted backend `mock.mjs`, runner, response builders, `checks/`), `adapters/` (`ui5` Playwright + the boot page, `agent` + the vendored mcp-server client, `webcomponent`, `headless` stub) | +| `conformance/apps/` | the conformance apps (ABAP + cap2UI5) and their abaplint config | +| `conformance/hosts/` | `node-runtime` (build + serve) and `cap2ui5` (a CAP project) reference hosts | +| `traffic/` | recorded traffic per backend: `suite.json`, `ui5-frontend.json`, `agent-client.json` | +| `scripts/` | recorder, runner, generators, `vendor-agent-client.mjs`; `scripts/lib/ui5-frontend.mjs` runs the UI5 frontend's request code in Node | +| `tools/portable-census/` | the census scripts behind the portable profile, as run | +| `test/` | `node:test` - `npm test` | + +## Gates + +`npm test` is the gate: schemas (shipped validator vs ajv, strict mode), +recorded traffic, cross-backend equality, docs links and anchors, the +portable profile, the conformance apps' abapGit format, the CLI, the +full backend suite against both reference backends (`PROTOCOL_SKIP_BACKENDS=1` +skips those; the cap2UI5 run skips itself when its host is not installed), +and the frontend suite against the agent client (always) and the UI5 SPA +(when an abap2UI5 checkout and a Chromium are there; +`PROTOCOL_SKIP_BROWSER=1` skips it, `PROTOCOL_REQUIRE_BROWSER=1` fails +without them). CI runs the same on Node 22 and 24, and the UI5 SPA in a +job of its own with Chromium. Never `playwright install` in a sandbox that +has browsers already - point `CHROMIUM_BIN` at one. + +## Style + +ES modules, Node 22+, no runtime dependencies in `conformance/backend/` +and `conformance/frontend/` (the browser adapters import `playwright-core` +lazily). English, ASCII in source files - the vendored copies under +`conformance/frontend/adapters/vendor/` are byte-equal to upstream and +exempt. Markdown wrapped at ~78 columns. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0706f27 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +## Unreleased + +- **Frontend conformance suite** (`abap2ui5-conformance frontend --adapter + ui5|agent|webcomponent [--profile core|portable|ui5|semantic]`, library + `runFrontendSuite`, also at `@abap2ui5/protocol/frontend`): a scripted + backend answers the frontend under test from the recorded traffic plus + synthetic edge cases (PROTOCOL 3, no PROTOCOL, 500 with markup, CSRF token + layer, unknown follow-up action, NEST slots, a model push of another + app); 81 checks (66 MUST, 15 SHOULD) over the request side (envelope, ID + continuation, event arguments, model delta, CONFIG cadence, headers, + stateful session id, CSRF handshake, no retry of a 500, one roundtrip at + a time) and the response side (PROTOCOL, unknown keys, view slots, model + push, follow-up actions, messages, router, errors) per profile. +- **Adapters:** the UI5 SPA (abap2UI5 `app/webapp`) in Chromium via + Playwright, UI5 from the `@openui5` npm packages (no CDN); the agent + client of abap2UI5/mcp-server (vendored at `ea4e9fa`, `npm run + vendor:agent`); the UI5 Web Components frontend (`dist/abap2ui5-wc.js`); + a documented stub for the headless ABAP simulator (no HTTP seam yet). +- **Results** ([conformance/RESULTS.md](conformance/RESULTS.md#frontend-suite)): + the UI5 SPA passes every MUST but `portable.box-details` (message box + details empty on OpenUI5 >= 1.120); the agent client fails 5 MUSTs. +- **Specification revision 0.2** - the frontend side made explicit where the + suite found it implicit, each with its source: a frontend sends event + arguments as raw JSON values and never converts them itself; an app start + names its class; `MODEL` carries only edited attributes, a pushed value is + no edit; a table cell travels as `__delta` (SHOULD) or as the whole + table; `sap-contextid-accept: header` SHOULD; the session id is kept by + every HTTP frontend; "one roundtrip at a time" covers programs driving a + frontend; a PROTOCOL mismatch adopts nothing; an error body is shown + verbatim, its markup not interpreted; message box details are shown with + the box; the UI5 frontend is no longer called "a portable frontend by + construction" - it renders every portable app but fails a view on an + element it cannot load (`profiles/portable.md`). Every frontend rule names + its frontend checks. +- **`spec/open-questions.md`**: the provisional decisions for the + maintainer - the NEST rule, a new draft id per response, the URL in the + error body, boolean arguments for non-ABAP backends, the detail of an + app's error, and five from the frontend suite. +- CI: a `frontend` job (Chromium, abap2UI5 `app/webapp` at `5d7e91f` (main)) + runs the UI5 SPA; `npm test` runs the agent client always. + +## 0.1.0 - 2026-10-03 + +First version. + +- **Specification** of protocol 2 (revision 0.1), derived from abap2UI5 + 1.146.0: core (`spec/core.md`), transport, request, response, actions, + sessions, navigation, errors, versioning - every normative statement + traced to its source. +- **View profiles:** UI5 (`profiles/ui5.md`); portable v1 + (`profiles/portable.md`, `portable-v1.json`, `portable-coverage.md`) from a + census of 247 core apps and 642 samples-controls ports; semantic - agent + snapshot v1, moved here from abap2UI5/mcp-server as the normative version. +- **JSON Schemas** (2020-12) for the request, the response, the snapshot and + the portable profile. +- **Backend conformance suite** (`abap2ui5-conformance backend`, library + `runBackendSuite`): 75 checks, profiles `core` and `ui5`. +- **Conformance apps** `Z2UI5_CL_CONF_*` in ABAP (abapGit) and as cap2UI5 + apps; reference hosts for `@abap2ui5/node-runtime` and cap2UI5. +- **Recorded traffic** of both reference backends (suite, UI5 frontend code, + agent client). Both pass every MUST; cap2UI5 warns on `error.details`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fc563b2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +# CLAUDE.md + +All project guidance lives in **[AGENTS.md](AGENTS.md)** — the single source of +truth for this repository (what is normative and how it is derived, the +spec/schema/conformance layout, the generated sections, the reference +backends and the test gates). + +Read `AGENTS.md` before making any change. diff --git a/README.md b/README.md index e3b925c..3643108 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,122 @@ -# protocol -test +# abap2UI5 protocol + +**The written, versioned contract between abap2UI5 frontends and backends - +with JSON Schemas and conformance suites, so either side can be swapped.** + +abap2UI5 is server-driven UI ("UI5 over the wire"): the browser frontend +sends one JSON roundtrip per user event - the event, the model delta, the +draft id - and the backend runs the app's `main( )` and answers with views, +the model, messages and frontend actions. That protocol is spoken today by +three backends (abap2UI5 on an ABAP system, the same framework transpiled to +Node as `@abap2ui5/node-runtime`, cap2UI5 on CAP) and three frontends (the +UI5 SPA, the MCP server's agent client, the ABAP frontend simulator). This +repository writes it down. + +## The layers + +``` + profiles/ view profiles - what is inside a view string + ui5 full UI5 XML + the .eB/.eF wire forms + the boot page (the UI5 SPA) + portable a 61-control subset every renderer renders (Web Components, native, ...) + semantic agent snapshot v1 - the screen as data (agents, test drivers) + --------------------------------------------------------------------------------- + spec/ the core protocol - mandatory for every frontend and backend + transport one endpoint, GET page / POST roundtrip, headers, CSRF, HEAD, sap-contextid + request S_FRONT, events and their arguments, the model delta (__delta), CONFIG + response S_FRONT, T_SYSTEM / T_CUSTOM, the five view slots, MODEL, processing order + actions follow-up actions, messages + sessions drafts, draft ids as snapshots, expiry, failed roundtrips + navigation app start resolution, app stack, routes, app-state hash, ROUTER action + errors the 500 text body and what a frontend does with it + versioning PROTOCOL 2, compatible changes + schema/ JSON Schemas (2020-12): request, response, snapshot, portable profile + conformance/ + backend the backend suite - plays the frontend over HTTP (implemented) + frontend the frontend suite - plays the backend, adapters for (implemented) + the UI5 SPA (Playwright), the agent client, the + Web Components frontend + apps the conformance apps: ABAP classes + cap2UI5 twins + hosts the two reference backends, started with the apps deployed + traffic/ real traffic of both reference backends and three frontends +``` + +Start with [spec/core.md](spec/core.md). Every normative statement cites the +source file and method it was derived from ([spec/README.md](spec/README.md#sources)). + +## Status + +- **Protocol 2**, specification revision 0.2, derived from abap2UI5 1.146.0 + (commit `b812079`). +- **Backend suite: 75 checks** (61 MUST, 14 SHOULD; 68 core, 7 UI5 profile). + Green against both reference backends - `node-runtime` 75/75, + `cap2ui5` 74/75 with one SHOULD warning ([conformance/RESULTS.md](conformance/RESULTS.md)). +- **Schemas** validate all 334 recorded requests, 320 responses and 18 agent + snapshots, checked by the shipped validator and by ajv. +- **Portable profile v1** filled from a census of 247 core apps (73.7 % by + controls, 64.8 % run unchanged). +- **Frontend suite: 81 checks** (66 MUST, 15 SHOULD; 66 core, 8 portable, + 4 UI5, 3 semantic), scripted from the recorded traffic. The official UI5 + SPA passes every MUST but one - message box details stay empty on OpenUI5 + >= 1.120; the agent client fails 5 MUSTs (no PROTOCOL check, no + `sap-contextid`, overlapping acts, popups kept across an app change, error + markup stripped) ([conformance/RESULTS.md](conformance/RESULTS.md#frontend-suite)). +- Decisions still open for the maintainer: [spec/open-questions.md](spec/open-questions.md). +- Not yet run against an ABAP system. + +## Run the backend suite + +```bash +npm ci + +# against any backend that serves the conformance apps +npx abap2ui5-conformance backend --url http://localhost:3000/ --profile ui5 + +# against the reference backends +npm run conformance:node-runtime # transpiles the ABAP apps, serves, runs +npm ci --prefix conformance/hosts/cap2ui5 +npm run conformance:cap2ui5 +``` + +To test your backend: deploy the [conformance apps](conformance/apps/README.md) +(abapGit for ABAP; the JS twins for cap2UI5), point `--url` at the endpoint. +Options, the library API and the check list: +[conformance/backend/README.md](conformance/backend/README.md). + +## Run the frontend suite + +```bash +npx abap2ui5-conformance frontend --adapter ui5 # the UI5 SPA (ABAP2UI5_HOME=) in Chromium +npx abap2ui5-conformance frontend --adapter agent # the agent client of abap2UI5/mcp-server +``` + +Adapters, options and the check list: +[conformance/frontend/README.md](conformance/frontend/README.md). + +## Develop + +```bash +npm test # schemas, traffic, docs, portable profile, CLI, both backends, + # the frontend suite (agent client; UI5 SPA with a checkout + Chromium) + # (PROTOCOL_SKIP_BACKENDS=1 / PROTOCOL_SKIP_BROWSER=1 skip the slow parts) +npm run record # re-record traffic/ from both backends +npm run lint:abap # abaplint over the ABAP conformance apps +npm run generate # regenerate the check list and the portable-profile sections +``` + +Node 22 or later. The package has no runtime dependencies; the dev +dependencies are the reference backend (`@abap2ui5/node-runtime`, the +transpiler, express), abaplint, ajv, and for the UI5 adapter +`playwright-core` and the `@openui5/sap.m` source packages (UI5 without a +CDN). + +## Related + +[abap2UI5](https://github.com/abap2UI5/abap2UI5) (backend + UI5 frontend) · +[cap2UI5](https://github.com/cap2UI5/cap2UI5) · +[mcp-server](https://github.com/abap2UI5/mcp-server) (agent client, snapshot implementation) · +[headless-frontend](https://github.com/abap2UI5/headless-frontend) · +[linter](https://github.com/abap2UI5/linter) + +## License + +MIT diff --git a/conformance/README.md b/conformance/README.md new file mode 100644 index 0000000..f6866b8 --- /dev/null +++ b/conformance/README.md @@ -0,0 +1,22 @@ +# Conformance + +Conformance is tested in both directions, so backends and frontends can be +swapped independently: + +| Suite | Plays | Tests | Status | +|---|---|---|---| +| [backend/](backend/README.md) | the frontend | a backend, over HTTP, against the conformance apps | implemented - 75 checks, `abap2ui5-conformance backend` | +| [frontend/](frontend/README.md) | the backend | a frontend, by scripted responses (recorded traffic + synthetic edge cases), through an adapter per frontend | implemented - 81 checks, `abap2ui5-conformance frontend --adapter ui5\|agent\|webcomponent` | + +The **conformance apps** ([apps/](apps/README.md)) are what a backend serves +for the backend suite: small apps with exactly specified behaviour, shipped +as ABAP classes (for abap2UI5 and every backend that runs ABAP apps) and as +cap2UI5 JavaScript apps. + +The **reference hosts** ([hosts/](hosts/)) start the two reference backends +with the apps deployed: `hosts/node-runtime` (`@abap2ui5/node-runtime`, the +ABAP apps transpiled on the fly) and `hosts/cap2ui5` (a minimal CAP project +with `@cap2ui5/cds-plugin` and the JavaScript apps). + +Results per backend and per frontend, and the differences found: +[RESULTS.md](RESULTS.md). diff --git a/conformance/RESULTS.md b/conformance/RESULTS.md new file mode 100644 index 0000000..b7cc5a3 --- /dev/null +++ b/conformance/RESULTS.md @@ -0,0 +1,195 @@ +# Results + +Backend suite, profile `ui5` (75 checks: 68 core, 7 UI5-profile; 61 MUST, +14 SHOULD), run 2026-10-03 with `npm run conformance:`. The traffic +of these runs is recorded in [`../traffic/`](../traffic/). + +| Backend | Version | Apps | Pass | Fail | Warn | Skip | Verdict | +|---|---|---|---:|---:|---:|---:|---| +| `node-runtime` | `@abap2ui5/node-runtime` 1.146.0 (abap2UI5 1.146.0, transpiled) | the ABAP apps, transpiled with `@abaplint/transpiler-cli` 2.13.93 | 75 | 0 | 0 | 0 | conformant, `core` and `ui5` | +| `cap2ui5` | `@cap2ui5/cds-plugin` 0.4.0 on `@abap2ui5/node-runtime` 1.146.0, `@sap/cds` 10 | the JavaScript apps | 74 | 0 | 1 | 0 | conformant, `core` and `ui5` | + +Per area (both backends identical except `errors`): + +| Area | Checks | node-runtime | cap2ui5 | +|---|---:|---|---| +| transport | 8 | 8 pass | 8 pass | +| response | 8 | 8 pass | 8 pass | +| event | 5 | 5 pass | 5 pass | +| model | 10 | 10 pass | 10 pass | +| message | 4 | 4 pass | 4 pass | +| slots | 7 | 7 pass | 7 pass | +| action | 3 | 3 pass | 3 pass | +| nav / route | 14 | 14 pass | 14 pass | +| session | 5 | 5 pass | 5 pass | +| error | 4 | 4 pass | 3 pass, 1 warn (`error.details`) | +| ui5 | 7 | 7 pass | 7 pass | + +The ABAP system itself (abap2UI5 on NetWeaver / ABAP Cloud) has not been run +yet - it needs a system with the apps pulled in by abapGit; the transpiled +runtime is the same framework source. + +## Differences between the two backends + +`test/traffic.test.mjs` compares the two recordings check by check - status, +media type and JSON body, draft ids normalized - and finds them +**identical** apart from the following. Both are the same framework +(cap2UI5 hosts `@abap2ui5/node-runtime` rather than porting it), so this +was the expected outcome; what differs is what the host adds around it. + +1. **Error body of a JavaScript app's exception** - *filed difference, + within the spec.* A `throw` in a cap2UI5 JavaScript app is no ABAP + exception: it passes the framework's `CATCH cx_root` and reaches the + plugin's own catch, which answers `500 text/plain` with + `roundtrip failed ()` and logs the cause server-side + (cap2UI5 `plugin/cds-plugin.js`, `roundtrip`). Status, media type and the + draft handling are the framework's (`error.app-exception`, + `session.failed-roundtrip-not-persisted` pass); only the body text names + nothing, so `error.details` (SHOULD) warns. + *Spec decision:* the content of an error body is not normative + ([../spec/errors.md](../spec/errors.md#the-error-response)); naming the + failure is a SHOULD with "MAY hide details on purpose", which is exactly + the reference's own `check_hide_error_details`. *Filed for cap2UI5:* the + masking is deliberate (no reconnaissance for clients), but it differs from + the framework's errors on the same route (an unknown draft shows the + framework's full body) - a project setting, or the user exit's + `check_hide_error_details`, could decide both the same way. +2. **`Content-Type` charset spelling of that body** - `text/plain; + charset=utf-8` (cap2UI5, express) vs `text/plain; charset=UTF-8` + (framework). Media types are case-insensitive in their parameters; not a + difference the spec cares about. + +## Spec decisions taken while running the suite + +Places where the implementations made a choice the specification now +records - as a rule, or as an implementation note: + +- **Only bound attributes are the model.** The called app's `MODEL` holds + `INPUT`/`OUTPUT`, not the unbound `HAS_PREV` - normative + ([../spec/response.md](../spec/response.md#model)). +- **An app that binds nothing sends no `MODEL`, even with a display** - + normative exception to "a display carries the model". +- **Draft ids repeat after `nav_app_leave`.** Leaving twice to the same + caller answered the same id twice - implementation note; a frontend must + not assume uniqueness ([../spec/sessions.md](../spec/sessions.md#draft-ids)). +- **An earlier draft id continues from its own snapshot** - normative + (retry safety, [../spec/sessions.md](../spec/sessions.md#drafts-are-snapshots)). +- **Unknown draft on an event = error; on a route = fresh start + toast** - + normative, both behaviours of the reference. +- **The request URL is reflected unescaped into the 500 body** - safe only + as `text/plain` + `nosniff` + a frontend that renders text; recorded as an + implementation note, the headers as SHOULD + ([../spec/errors.md](../spec/errors.md#the-error-response)). +- **`DEFAULT` routing mode on every hop to an app without a mode** - + normative ([../spec/navigation.md](../spec/navigation.md#the-router-action)). +- **Event argument conversion** (`true` -> `X`, `null`/`false` -> empty, + objects -> compact JSON text) - normative, the same on both backends. + +## Frontend suite + +Frontend suite, 81 checks (66 MUST, 15 SHOULD; 66 core, 8 portable, 4 UI5, +3 semantic), run 2026-10-03 with +`abap2ui5-conformance frontend --adapter ` (each frontend at its +widest profile). The scripted backend answers from +[`../traffic/node-runtime/`](../traffic/node-runtime/) plus synthetic edge +cases. A skip is no verdict: outside the profile, a capability the +frontend does not have (a URL, a DOM, timers), or an interaction it cannot +perform. + +| Frontend | Version | Profile | Pass | Fail | Warn | Skip | Verdict | +|---|---|---|---:|---:|---:|---:|---| +| UI5 SPA (`ui5`) | abap2UI5 1.146.0 `b812079` `app/webapp` (identical at main `5d7e91f`, which CI pins), OpenUI5 1.144.0 (npm), Chromium 141 | ui5 | 76 | 1 | 0 | 4 | one MUST deviation: `portable.box-details` | +| agent client (`agent`) | abap2UI5/mcp-server `lib/appclient.mjs` @ `ea4e9fa` (vendored) | semantic | 53 | 5 | 3 | 20 | not conformant: 5 MUSTs | +| Web Components (`webcomponent`) | abap2UI5/frontend-webcomponent 0.1.0, `dist/` built from `6997c40` (in development) | portable | 63 | 4 | 1 | 13 | work in progress: router not implemented, error markup stripped | +| headless ABAP simulator (`headless`) | - | - | - | - | - | - | not drivable: in-process, no HTTP seam ([frontend/README.md](frontend/README.md#adapters)) | + +The UI5 SPA run is stable (two consecutive runs, identical results) and +takes about two minutes; `test/frontend.test.mjs` pins both the UI5 and the +agent result check by check. + +### Findings - the UI5 SPA + +1. **Message box details are not shown on OpenUI5 >= 1.120** - + `portable.box-details`, MUST; *the frontend is wrong.* A + `MESSAGE_BOX` with `details` shows the text and an empty details area. + Evidence (OpenUI5 1.144.0): the box's VBox holds `Text` (visible), + `Link "View Details"` (hidden), `MessageStrip "Details could not be + loaded."` (hidden), `FormattedText` (visible, `htmlText` empty). + abap2UI5 `app/webapp/core/actions/ControlCall.js` `expandBoxDetails` + makes the `FormattedText` visible and hides the link - but + `sap.m.MessageBox` sets the details text only in the link's press handler + (`showDetails`, 1.144.0 `MessageBox.js` line 249; the same in 1.120.0), + while 1.71.80 set it when creating the control + (`new FormattedText().setVisible(false).setHtmlText(mOptions.details)`). + So the expansion works on 1.71 and leaves the details unreachable on + every release since the lazy fill - including the 1.144.0 the abap2UI5 + e2e suite pins. Fix belongs in `expandBoxDetails` (set the sanitized + details as `htmlText` itself, or press the link). Spec: details are shown + with the box ([../spec/actions.md](../spec/actions.md#messages); + alternative in [open question 7](../spec/open-questions.md#7-message-box-details-shown-or-behind-a-link)). +2. **An unknown control fails the view** - `portable.unknown-control`; + *the spec was wrong.* `` + ends in `ModuleError: failed to load 'com/example/conformance/Gadget.js'` + and the fatal overlay. The portable profile called the UI5 frontend "a + portable frontend by construction", which would require a placeholder; + but in the UI5 profile every UI5 element is in the vocabulary and one that + does not load is a failed view (and the frontend's SDK hint depends on + that). The profile now says the UI5 frontend renders every portable app + and binds the tolerance rule to the other renderers; the check skips a + UI5-profile frontend ([open question 6](../spec/open-questions.md#6-the-ui5-frontend-and-the-portable-tolerance-rule)). + +Everything else holds in the browser, with real controls: the envelope, +ID continuation (also an id seen before), raw JSON event arguments (`true` +stays `true`), the delta rules (whole scalars and structures, `__delta` +rows, nested `__delta`, the whole value beating a delta, the popup's own +model, no delta for pushed values, edits typed during a roundtrip kept), the +CONFIG cadence, `sap-contextid` kept, the CSRF handshake, no retry of a +500, one roundtrip at a time, the queue-last wire, PROTOCOL 3 refused +naming both numbers, unknown keys ignored, every slot rule including the +teardown on an APP change, model pushes only into the answering app's +slots, follow-up actions in order after rendering, an unknown action +skipped and logged, the box close event with the pressed action, the hash +under KEEP, Back restoring the caller's route, the app-state hash, and an +error body shown verbatim as text. + +### Findings - the agent client + +All five MUST failures are *the client's*; filed for abap2UI5/mcp-server. + +1. **No PROTOCOL check** (`response.protocol-mismatch`): a `PROTOCOL: 3` + response is adopted and described. `lib/appclient.mjs` `post` checks + status, JSON and `S_FRONT` only. +2. **No stateful session id** (`transport.contextid-kept`): the + `sap-contextid` response header is never read or sent back; a stateful + ABAP app would meet a fresh work process on every act + ([open question 9](../spec/open-questions.md#9-stateful-sessions-and-frontends-without-a-browser)). +3. **Overlapping acts** (`transport.one-at-a-time`): a second `act` while + the first is in flight posts at once, continuing the same draft id + ([open question 8](../spec/open-questions.md#8-one-roundtrip-at-a-time-for-frontends-driven-by-a-program)). +4. **Popups survive an app change** (`slots.app-change`): + `lib/snapshot.mjs` `applyResponse` tears POPUP/POPOVER down only on a + MAIN display or a destroy; a popup app answering with another `APP` leaves + the previous app's popover in the snapshot. +5. **Error markup interpreted** (`error.as-text`): `errorText` extracts a + `
`, strips tags and decodes entities - written for the old HTML
+   error page; the body is `text/plain` now. `bold` arrives as
+   `bold`.
+
+Warnings: no CSRF token handshake (`transport.csrf-token`); edits made with
+an `act` while another is in flight are dropped, because `act` clears all
+pending edits of the model after its response, not only the ones it sent
+(`model.pending-survive-push`); the PROTOCOL message (follows from 1).
+Skipped: the URL, DOM and focus checks (no browser), a nested-table cell
+(the snapshot does not describe nested tables), a programmatic model edit.
+
+### Findings - the Web Components frontend (in development)
+
+Run against the build of its development branch at the time; recorded for
+its authors, not as a verdict. It passes every portable-profile check that
+applies, including the tolerance rule, the NEST placeholder rule and the
+box details. Failing: the `ROUTER` action and the URL hash are not
+implemented yet (`router.keep`, `router.back-restores`, `router.app-state`;
+`router.hash-sent` warns), and error markup is stripped (`error.as-text`) -
+it vendors the agent client's `errorText`. Table cells were not addressed
+(its DOM exposes control ids, not binding paths) and the box close is not
+wired in the adapter yet.
diff --git a/conformance/apps/README.md b/conformance/apps/README.md
new file mode 100644
index 0000000..9fedba9
--- /dev/null
+++ b/conformance/apps/README.md
@@ -0,0 +1,131 @@
+# Conformance apps
+
+The backend suite drives a backend through these apps. A backend that wants
+to be tested **serves all of them under exactly these class names** (the
+CLI's `--app KEY=CLASS` maps a name when a backend cannot use it). Their
+behaviour is specified here, observable on the wire only: the view XML each
+app displays is the UI5 profile's (the suite reads it only in `ui5`-profile
+checks), the **model** and the **actions** are what the core checks compare.
+
+Reference implementations - behaving identically, byte for byte on the wire
+(`test/traffic.test.mjs` compares the recorded traffic):
+
+- [`abap/`](abap/) - ABAP classes in abapGit format (`.clas.abap` +
+  `.clas.xml`), built with `z2ui5_cl_ui5_view_builder`, abaplint-clean
+  against abap2UI5 with its shared app rule set
+  ([`abaplint.jsonc`](abaplint.jsonc), `npm run lint:abap`) and clean in the
+  abap2UI5 linter. Pull them into any abap2UI5 system with abapGit, or
+  transpile them for `@abap2ui5/node-runtime`
+  ([`../hosts/node-runtime/build.mjs`](../hosts/node-runtime/build.mjs)).
+- [`cap2ui5/`](cap2ui5/) - the same apps as cap2UI5 JavaScript apps
+  (`defineApp`, [`../hosts/cap2ui5/`](../hosts/cap2ui5/package.json)).
+
+Conventions for every app:
+
+- The app displays its MAIN view (a `sap.m.Page`) on every roundtrip on
+  which `check_on_navigated( )` is true, and only then (and on `RERENDER`).
+- Model names below are the JSON keys (upper case); in the app they are
+  lower-case attributes.
+- "Pushes the model" means: the response carries `MODEL`, no display.
+  "No MODEL" means the key is absent.
+- An event not listed is ignored (nothing changes, no MODEL, no action).
+
+## Z2UI5_CL_CONF_ECHO
+
+Event roundtrips and their arguments.
+
+| | |
+|---|---|
+| Model | `COUNT` (integer, 0), `LAST_EVENT` (string, ""), `LAST_ARGS` (string, "") |
+| View wires | `ECHO` with the arguments `'alpha'`, `'beta'`; `NOOP`; `PUSH`; `RERENDER` |
+| `ECHO` | `COUNT` + 1; `LAST_EVENT` = the event name; `LAST_ARGS` = the event's arguments as the app receives them, joined with `\|` |
+| `PUSH` | `COUNT` + 100 - pushes the model |
+| `RERENDER` | displays MAIN again (MAIN display + MODEL) |
+| `NOOP` | nothing - no MODEL, no action |
+
+## Z2UI5_CL_CONF_BIND
+
+The model delta. `CHECK` writes what the backend holds into `SUMMARY`.
+
+| | |
+|---|---|
+| Model (initial) | `NAME` "start", `QTY` 1 (integer), `FLAG` false (boolean), `S_ADDR` { `CITY` "Berlin", `ZIP` "10115" }, `T_ITEMS` [{ `ID` 1, `TEXT` "one", `DONE` false }, { 2, "two", false }, { 3, "three", false }], `SUMMARY` "" |
+| View | Input `{/NAME}`, StepInput `{/QTY}`, CheckBox `{/FLAG}`, Inputs `{/S_ADDR/CITY}`, `{/S_ADDR/ZIP}`, a Table over `{/T_ITEMS}` with an Input `{TEXT}` and a CheckBox `{DONE}` per row |
+| `CHECK` | `SUMMARY` = `NAME;QTY;FLAG;CITY;ZIP;ROWS` with `FLAG` as `X` or empty and `ROWS` = `ID:TEXT:DONE` per row (`DONE` as `X` or empty) joined with `,` - initially `start;1;;Berlin;10115;1:one:,2:two:,3:three:` |
+| `ADD_ROW` | appends { `ID` = rows + 1, `TEXT` "new", `DONE` false }, then as `CHECK` |
+
+## Z2UI5_CL_CONF_MSG
+
+Messages.
+
+| | |
+|---|---|
+| Model | `LAST_ACTION` (string, "") |
+| `TOAST` | `["MESSAGE_TOAST", "show", "conformance toast"]` |
+| `BOX` | `["MESSAGE_BOX", "error", "conformance box"]` |
+| `BOX_CONFIRM` | `["MESSAGE_BOX", "confirm", "conformance confirm", { "actions": ["OK", "CANCEL"], "onClose": "BOX_CLOSED" }]` |
+| `BOX_CLOSED` | `LAST_ACTION` = the first event argument (the pressed action) |
+
+## Z2UI5_CL_CONF_SLOTS
+
+The slots besides MAIN.
+
+| | |
+|---|---|
+| Model | `POPUP_TEXT` (string, "") - bound in the popup |
+| View | Buttons `btn_popup`, `btn_popover` (ids), `Nest`, `Nest2`; VBoxes `nest_anchor`, `nest2_anchor` |
+| `POPUP_OPEN` | POPUP display: a `Dialog` titled "conformance popup" with an Input `{/POPUP_TEXT}`, a button wired to `POPUP_CLOSE` and one with the frontend close (`cs_event-popup_close`) |
+| `POPUP_REPLACE` | displays the popup twice in one roundtrip ("first", then "second") - one POPUP display, "second" |
+| `POPUP_CLOSE` | `["VIEW_SLOTS", "destroy", "POPUP"]` |
+| `MAIN_AND_POPUP` | calls `popup_display` ("with main") and then `view_display` - displays MAIN, then POPUP |
+| `POPOVER_OPEN` | POPOVER display: a `Popover` titled "conformance popover", options `{ "openById": "btn_popover" }` |
+| `POPOVER_CLOSE` | `["VIEW_SLOTS", "destroy", "POPOVER"]` |
+| `NEST_OPEN` / `NEST2_OPEN` | NEST / NEST2 display: a view with a VBox, text "conformance NEST" / "conformance NEST2", options `{ "id": "nest_anchor" \| "nest2_anchor", "methodInsert": "addItem", "methodDestroy": "removeAllItems" }` |
+| `NEST_CLOSE` / `NEST2_CLOSE` | destroy NEST / NEST2 |
+
+## Z2UI5_CL_CONF_NAV and Z2UI5_CL_CONF_NAV_TGT
+
+The app stack.
+
+| | |
+|---|---|
+| NAV model | `RESULT` (string, ""), `RETURNS` (integer, 0) |
+| NAV `CALL` | `nav_app_call` of a new `Z2UI5_CL_CONF_NAV_TGT` with `INPUT` = "from caller" |
+| NAV on a navigated roundtrip with the event `RETURNED` | `RESULT` = the target's `OUTPUT` (read from `get_app_prev( )`), `RETURNS` + 1, then display |
+| NAV_TGT model | `INPUT` (string), `OUTPUT` (string, "") - `HAS_PREV` is an attribute it does not bind |
+| NAV_TGT view | a Page with `showNavButton` = whether there is an app to go back to, `navButtonPress` = the leave wire (`_event_nav_app_leave( )`) |
+| NAV_TGT `DONE` | `OUTPUT` = `INPUT` + "!", `nav_app_leave( event = 'RETURNED' )` |
+
+## Z2UI5_CL_CONF_ROUTE
+
+Hash routing.
+
+| | |
+|---|---|
+| Model | `COUNT` (integer, 0) |
+| first roundtrip | `follow_up_action( cs_event-hash_routing )` with `KEEP` |
+| `COUNT` | `COUNT` + 1 |
+| `CALL` | `nav_app_call` of a new `Z2UI5_CL_CONF_NAV_TGT` with `INPUT` = "from route" |
+
+## Z2UI5_CL_CONF_ERROR
+
+The error response.
+
+| | |
+|---|---|
+| Model | `COUNT` (integer, 0) |
+| `FAIL` | `COUNT` + 1, then an unhandled exception (ABAP: a failing conversion, `CX_SY_CONVERSION_NO_NUMBER`; JavaScript: `throw new Error("CONFORMANCE_FAILURE")`) |
+| `COUNT` | `COUNT` + 1 |
+
+## Z2UI5_CL_CONF_ACTIONS
+
+Follow-up actions.
+
+| | |
+|---|---|
+| Model | `TICKS` (integer, 0), bound to an Input with id `inp` |
+| View | also a button with the wired frontend action `SET_TITLE` "wired title" (`follow_up_action( )` as attribute) |
+| `FOCUS` | `["SET_FOCUS", "inp"]` |
+| `SEVERAL` | `["SET_TITLE", "conformance title"]`, `["MESSAGE_TOAST", "show", "between"]`, `["SET_FOCUS", "inp"]` - in this order |
+| `TIMER` | `["START_TIMER", "TICK", "500"]` |
+| `TICK` | `TICKS` + 1 |
diff --git a/conformance/apps/abap/z2ui5_cl_conf_actions.clas.abap b/conformance/apps/abap/z2ui5_cl_conf_actions.clas.abap
new file mode 100644
index 0000000..e4bacdd
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_actions.clas.abap
@@ -0,0 +1,88 @@
+" Conformance app ACTIONS - follow-up actions: what an app queues for the
+" frontend to do once the view is rendered (T_CUSTOM), in the order it
+" queued them, and a frontend event wired into the view. Behaviour:
+" conformance/apps/README.md, section ACTIONS.
+CLASS z2ui5_cl_conf_actions DEFINITION PUBLIC FINAL CREATE PUBLIC.
+
+  PUBLIC SECTION.
+    INTERFACES z2ui5_if_app.
+
+    DATA ticks TYPE i.
+
+  PROTECTED SECTION.
+    DATA client TYPE REF TO z2ui5_if_client.
+
+    METHODS view_display.
+    METHODS on_event.
+
+  PRIVATE SECTION.
+ENDCLASS.
+
+
+CLASS z2ui5_cl_conf_actions IMPLEMENTATION.
+
+  METHOD z2ui5_if_app~main.
+
+    me->client = client.
+    IF client->check_on_navigated( ).
+      view_display( ).
+    ELSEIF client->check_on_event( ).
+      on_event( ).
+    ENDIF.
+
+  ENDMETHOD.
+
+  METHOD view_display.
+
+    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
+
+            )->ele( `Page`
+                )->a( n = `title` v = `conformance - actions`
+
+                )->tag( `Input`
+                    )->a( n = `id`    v = `inp`
+                    )->a( n = `value` v = client->_bind( ticks )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Focus`
+                    )->a( n = `press` v = client->_event( `FOCUS` )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Several`
+                    )->a( n = `press` v = client->_event( `SEVERAL` )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Timer`
+                    )->a( n = `press` v = client->_event( `TIMER` )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Title here`
+                    )->a( n = `press` v = client->follow_up_action( val   = z2ui5_if_client=>cs_event-set_title
+                                                                   t_arg = VALUE #( ( `wired title` ) ) ) ).
+
+    client->view_display( view->stringify( ) ).
+
+  ENDMETHOD.
+
+  METHOD on_event.
+
+    CASE client->get_event( ).
+      WHEN `FOCUS`.
+        client->follow_up_action( val   = z2ui5_if_client=>cs_event-set_focus
+                                  t_arg = VALUE #( ( `inp` ) ) ).
+      WHEN `SEVERAL`.
+        " three actions of two kinds - they must arrive in this order
+        client->follow_up_action( val   = z2ui5_if_client=>cs_event-set_title
+                                  t_arg = VALUE #( ( `conformance title` ) ) ).
+        client->message_toast_display( `between` ).
+        client->follow_up_action( val   = z2ui5_if_client=>cs_event-set_focus
+                                  t_arg = VALUE #( ( `inp` ) ) ).
+      WHEN `TIMER`.
+        client->follow_up_action( val   = z2ui5_if_client=>cs_event-start_timer
+                                  t_arg = VALUE #( ( `TICK` ) ( `500` ) ) ).
+      WHEN `TICK`.
+        ticks = ticks + 1.
+    ENDCASE.
+
+  ENDMETHOD.
+
+ENDCLASS.
diff --git a/conformance/apps/abap/z2ui5_cl_conf_actions.clas.xml b/conformance/apps/abap/z2ui5_cl_conf_actions.clas.xml
new file mode 100644
index 0000000..ccd3989
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_actions.clas.xml
@@ -0,0 +1,16 @@
+
+
+ 
+  
+   
+    Z2UI5_CL_CONF_ACTIONS
+    E
+    protocol conformance - follow-up actions
+    1
+    X
+    X
+    X
+   
+  
+ 
+
diff --git a/conformance/apps/abap/z2ui5_cl_conf_bind.clas.abap b/conformance/apps/abap/z2ui5_cl_conf_bind.clas.abap
new file mode 100644
index 0000000..1acf951
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_bind.clas.abap
@@ -0,0 +1,159 @@
+" Conformance app BIND - the model delta: a scalar, a number, a boolean, a
+" structure and a table edited by row (__delta). CHECK writes what the
+" backend holds into SUMMARY, so a client can see which edits arrived.
+" Behaviour: conformance/apps/README.md, section BIND.
+CLASS z2ui5_cl_conf_bind DEFINITION PUBLIC FINAL CREATE PUBLIC.
+
+  PUBLIC SECTION.
+    INTERFACES z2ui5_if_app.
+
+    TYPES:
+      BEGIN OF ty_s_addr,
+        city TYPE string,
+        zip  TYPE string,
+      END OF ty_s_addr.
+    TYPES:
+      BEGIN OF ty_s_item,
+        id   TYPE i,
+        text TYPE string,
+        done TYPE abap_bool,
+      END OF ty_s_item.
+    TYPES ty_t_item TYPE STANDARD TABLE OF ty_s_item WITH EMPTY KEY.
+
+    DATA name    TYPE string.
+    DATA qty     TYPE i.
+    DATA flag    TYPE abap_bool.
+    DATA s_addr  TYPE ty_s_addr.
+    DATA t_items TYPE ty_t_item.
+    DATA summary TYPE string.
+
+  PROTECTED SECTION.
+    DATA client TYPE REF TO z2ui5_if_client.
+
+    METHODS view_display.
+    METHODS on_event.
+    METHODS summary_build.
+    METHODS model_init.
+
+  PRIVATE SECTION.
+ENDCLASS.
+
+
+CLASS z2ui5_cl_conf_bind IMPLEMENTATION.
+
+  METHOD z2ui5_if_app~main.
+
+    me->client = client.
+    IF client->check_on_init( ).
+      model_init( ).
+      view_display( ).
+    ELSEIF client->check_on_navigated( ).
+      view_display( ).
+    ELSEIF client->check_on_event( ).
+      on_event( ).
+    ENDIF.
+
+  ENDMETHOD.
+
+  METHOD view_display.
+
+    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` ).
+
+    DATA(page) = view->ele( `Page`
+        )->a( n = `title` v = `conformance - bind` ).
+
+    page->tag( `Input`
+        )->a( n = `id`    v = `name`
+        )->a( n = `value` v = client->_bind( name )
+    )->tag( `StepInput`
+        )->a( n = `id`    v = `qty`
+        )->a( n = `value` v = client->_bind( qty )
+    )->tag( `CheckBox`
+        )->a( n = `id`       v = `flag`
+        )->a( n = `text`     v = `Flag`
+        )->a( n = `selected` v = client->_bind( flag )
+    )->tag( `Input`
+        )->a( n = `id`    v = `city`
+        )->a( n = `value` v = client->_bind( s_addr-city )
+    )->tag( `Input`
+        )->a( n = `id`    v = `zip`
+        )->a( n = `value` v = client->_bind( s_addr-zip )
+    )->tag( `Text`
+        )->a( n = `id`   v = `summary`
+        )->a( n = `text` v = client->_bind( summary )
+    )->tag( `Button`
+        )->a( n = `text`  v = `Check`
+        )->a( n = `press` v = client->_event( `CHECK` )
+    )->tag( `Button`
+        )->a( n = `text`  v = `Add row`
+        )->a( n = `press` v = client->_event( `ADD_ROW` ) ).
+
+    page->ele( `Table`
+        )->a( n = `id`    v = `items`
+        )->a( n = `items` v = client->_bind( t_items )
+
+        )->ele( `columns`
+
+            )->tag( `Column`
+            )->tag( `Column`
+            )->tag( `Column`
+
+        )->end(
+        )->ele( `items`
+            )->ele( `ColumnListItem`
+                )->ele( `cells`
+
+                    )->tag( `Text`
+                        )->a( n = `text` v = `{ID}`
+                    )->tag( `Input`
+                        )->a( n = `value` v = `{TEXT}`
+                    )->tag( `CheckBox`
+                        )->a( n = `selected` v = `{DONE}` ).
+
+    client->view_display( view->stringify( ) ).
+
+  ENDMETHOD.
+
+  METHOD on_event.
+
+    CASE client->get_event( ).
+      WHEN `CHECK`.
+        summary_build( ).
+      WHEN `ADD_ROW`.
+        INSERT VALUE #( id   = lines( t_items ) + 1
+                        text = `new` ) INTO TABLE t_items.
+        summary_build( ).
+    ENDCASE.
+
+  ENDMETHOD.
+
+  METHOD summary_build.
+
+    DATA lt_rows TYPE string_table.
+
+    LOOP AT t_items INTO DATA(ls_item).
+      INSERT |{ ls_item-id }:{ ls_item-text }:{ ls_item-done }| INTO TABLE lt_rows.
+    ENDLOOP.
+
+    summary = |{ name };{ qty };{ flag };{ s_addr-city };{ s_addr-zip };| &&
+              concat_lines_of( table = lt_rows
+                               sep   = `,` ).
+
+  ENDMETHOD.
+
+  METHOD model_init.
+
+    name   = `start`.
+    qty    = 1.
+    s_addr = VALUE #( city = `Berlin`
+                      zip  = `10115` ).
+    t_items = VALUE #( ( id = 1 text = `one`   )
+                       ( id = 2 text = `two`   )
+                       ( id = 3 text = `three` ) ).
+
+  ENDMETHOD.
+
+ENDCLASS.
diff --git a/conformance/apps/abap/z2ui5_cl_conf_bind.clas.xml b/conformance/apps/abap/z2ui5_cl_conf_bind.clas.xml
new file mode 100644
index 0000000..1e3b35b
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_bind.clas.xml
@@ -0,0 +1,16 @@
+
+
+ 
+  
+   
+    Z2UI5_CL_CONF_BIND
+    E
+    protocol conformance - model delta
+    1
+    X
+    X
+    X
+   
+  
+ 
+
diff --git a/conformance/apps/abap/z2ui5_cl_conf_echo.clas.abap b/conformance/apps/abap/z2ui5_cl_conf_echo.clas.abap
new file mode 100644
index 0000000..b91091e
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_echo.clas.abap
@@ -0,0 +1,92 @@
+" Conformance app ECHO - first start, the event roundtrip with arguments,
+" a roundtrip that changes nothing, a model push without a view, and a
+" re-render. Behaviour: conformance/apps/README.md, section ECHO.
+CLASS z2ui5_cl_conf_echo DEFINITION PUBLIC FINAL CREATE PUBLIC.
+
+  PUBLIC SECTION.
+    INTERFACES z2ui5_if_app.
+
+    DATA count      TYPE i.
+    DATA last_event TYPE string.
+    DATA last_args  TYPE string.
+
+  PROTECTED SECTION.
+    DATA client TYPE REF TO z2ui5_if_client.
+
+    METHODS view_display.
+    METHODS on_event.
+
+  PRIVATE SECTION.
+ENDCLASS.
+
+
+CLASS z2ui5_cl_conf_echo IMPLEMENTATION.
+
+  METHOD z2ui5_if_app~main.
+
+    me->client = client.
+    IF client->check_on_navigated( ).
+      view_display( ).
+    ELSEIF client->check_on_event( ).
+      on_event( ).
+    ENDIF.
+
+  ENDMETHOD.
+
+  METHOD view_display.
+
+    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
+
+            )->ele( `Page`
+                )->a( n = `title` v = `conformance - echo`
+
+                )->tag( `Text`
+                    )->a( n = `id`   v = `count`
+                    )->a( n = `text` v = client->_bind( count )
+                )->tag( `Text`
+                    )->a( n = `id`   v = `last_event`
+                    )->a( n = `text` v = client->_bind( last_event )
+                )->tag( `Text`
+                    )->a( n = `id`   v = `last_args`
+                    )->a( n = `text` v = client->_bind( last_args )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Echo`
+                    )->a( n = `press` v = client->_event( val   = `ECHO`
+                                                         t_arg = VALUE #( ( `alpha` ) ( `beta` ) ) )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Nothing`
+                    )->a( n = `press` v = client->_event( `NOOP` )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Push`
+                    )->a( n = `press` v = client->_event( `PUSH` )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Render`
+                    )->a( n = `press` v = client->_event( `RERENDER` ) ).
+
+    client->view_display( view->stringify( ) ).
+
+  ENDMETHOD.
+
+  METHOD on_event.
+
+    CASE client->get_event( ).
+      WHEN `ECHO`.
+        count      = count + 1.
+        last_event = client->get_event( ).
+        last_args  = concat_lines_of( table = client->get( )-t_event_arg
+                                      sep   = `|` ).
+      WHEN `PUSH`.
+        count = count + 100.
+      WHEN `RERENDER`.
+        view_display( ).
+      WHEN OTHERS.
+        " NOOP and every unknown event: nothing bound changes, nothing is
+        " displayed - the response carries no MODEL and no action
+    ENDCASE.
+
+  ENDMETHOD.
+
+ENDCLASS.
diff --git a/conformance/apps/abap/z2ui5_cl_conf_echo.clas.xml b/conformance/apps/abap/z2ui5_cl_conf_echo.clas.xml
new file mode 100644
index 0000000..a023d7a
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_echo.clas.xml
@@ -0,0 +1,16 @@
+
+
+ 
+  
+   
+    Z2UI5_CL_CONF_ECHO
+    E
+    protocol conformance - event roundtrip and echo
+    1
+    X
+    X
+    X
+   
+  
+ 
+
diff --git a/conformance/apps/abap/z2ui5_cl_conf_error.clas.abap b/conformance/apps/abap/z2ui5_cl_conf_error.clas.abap
new file mode 100644
index 0000000..3708cb5
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_error.clas.abap
@@ -0,0 +1,68 @@
+" Conformance app ERROR - an unhandled exception in main( ). FAIL raises,
+" NOOP answers normally, so a client can show that a failed roundtrip
+" leaves the draft it started from usable. Behaviour:
+" conformance/apps/README.md, section ERROR.
+CLASS z2ui5_cl_conf_error DEFINITION PUBLIC FINAL CREATE PUBLIC.
+
+  PUBLIC SECTION.
+    INTERFACES z2ui5_if_app.
+
+    DATA count TYPE i.
+
+  PROTECTED SECTION.
+    DATA client TYPE REF TO z2ui5_if_client.
+
+    METHODS view_display.
+
+  PRIVATE SECTION.
+ENDCLASS.
+
+
+CLASS z2ui5_cl_conf_error IMPLEMENTATION.
+
+  METHOD z2ui5_if_app~main.
+
+    me->client = client.
+    IF client->check_on_navigated( ).
+      view_display( ).
+      RETURN.
+    ENDIF.
+
+    CASE client->get_event( ).
+      WHEN `FAIL`.
+        count = count + 1.
+        " an unhandled exception on purpose, raised by the language itself
+        " (CX_SY_CONVERSION_NO_NUMBER) rather than by a framework class an
+        " app must not name - the error body shows it with app and event
+        count = `CONFORMANCE_FAILURE`.
+      WHEN `COUNT`.
+        count = count + 1.
+    ENDCASE.
+
+  ENDMETHOD.
+
+  METHOD view_display.
+
+    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
+
+            )->ele( `Page`
+                )->a( n = `title` v = `conformance - error`
+
+                )->tag( `Text`
+                    )->a( n = `id`   v = `count`
+                    )->a( n = `text` v = client->_bind( count )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Fail`
+                    )->a( n = `press` v = client->_event( `FAIL` )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Count`
+                    )->a( n = `press` v = client->_event( `COUNT` ) ).
+
+    client->view_display( view->stringify( ) ).
+
+  ENDMETHOD.
+
+ENDCLASS.
diff --git a/conformance/apps/abap/z2ui5_cl_conf_error.clas.xml b/conformance/apps/abap/z2ui5_cl_conf_error.clas.xml
new file mode 100644
index 0000000..10bad01
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_error.clas.xml
@@ -0,0 +1,16 @@
+
+
+ 
+  
+   
+    Z2UI5_CL_CONF_ERROR
+    E
+    protocol conformance - error response
+    1
+    X
+    X
+    X
+   
+  
+ 
+
diff --git a/conformance/apps/abap/z2ui5_cl_conf_msg.clas.abap b/conformance/apps/abap/z2ui5_cl_conf_msg.clas.abap
new file mode 100644
index 0000000..a6ae7eb
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_msg.clas.abap
@@ -0,0 +1,80 @@
+" Conformance app MSG - messages: a toast, an error box, and a confirm box
+" whose closing raises a backend event with the pressed action as its
+" argument. Behaviour: conformance/apps/README.md, section MSG.
+CLASS z2ui5_cl_conf_msg DEFINITION PUBLIC FINAL CREATE PUBLIC.
+
+  PUBLIC SECTION.
+    INTERFACES z2ui5_if_app.
+
+    DATA last_action TYPE string.
+
+  PROTECTED SECTION.
+    DATA client TYPE REF TO z2ui5_if_client.
+
+    METHODS view_display.
+    METHODS on_event.
+
+  PRIVATE SECTION.
+ENDCLASS.
+
+
+CLASS z2ui5_cl_conf_msg IMPLEMENTATION.
+
+  METHOD z2ui5_if_app~main.
+
+    me->client = client.
+    IF client->check_on_navigated( ).
+      view_display( ).
+    ELSEIF client->check_on_event( ).
+      on_event( ).
+    ENDIF.
+
+  ENDMETHOD.
+
+  METHOD view_display.
+
+    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
+
+            )->ele( `Page`
+                )->a( n = `title` v = `conformance - messages`
+
+                )->tag( `Text`
+                    )->a( n = `id`   v = `last_action`
+                    )->a( n = `text` v = client->_bind( last_action )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Toast`
+                    )->a( n = `press` v = client->_event( `TOAST` )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Box`
+                    )->a( n = `press` v = client->_event( `BOX` )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Confirm`
+                    )->a( n = `press` v = client->_event( `BOX_CONFIRM` ) ).
+
+    client->view_display( view->stringify( ) ).
+
+  ENDMETHOD.
+
+  METHOD on_event.
+
+    CASE client->get_event( ).
+      WHEN `TOAST`.
+        client->message_toast_display( `conformance toast` ).
+      WHEN `BOX`.
+        client->message_box_display( text = `conformance box`
+                                     type = `error` ).
+      WHEN `BOX_CONFIRM`.
+        client->message_box_display( text    = `conformance confirm`
+                                     type    = `confirm`
+                                     onclose = `BOX_CLOSED`
+                                     actions = VALUE #( ( `OK` ) ( `CANCEL` ) ) ).
+      WHEN `BOX_CLOSED`.
+        last_action = client->get_event_arg( ).
+    ENDCASE.
+
+  ENDMETHOD.
+
+ENDCLASS.
diff --git a/conformance/apps/abap/z2ui5_cl_conf_msg.clas.xml b/conformance/apps/abap/z2ui5_cl_conf_msg.clas.xml
new file mode 100644
index 0000000..ea1e610
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_msg.clas.xml
@@ -0,0 +1,16 @@
+
+
+ 
+  
+   
+    Z2UI5_CL_CONF_MSG
+    E
+    protocol conformance - messages
+    1
+    X
+    X
+    X
+   
+  
+ 
+
diff --git a/conformance/apps/abap/z2ui5_cl_conf_nav.clas.abap b/conformance/apps/abap/z2ui5_cl_conf_nav.clas.abap
new file mode 100644
index 0000000..2bc7b2e
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_nav.clas.abap
@@ -0,0 +1,69 @@
+" Conformance app NAV - the app stack: CALL hands the screen to
+" Z2UI5_CL_CONF_NAV_TGT, which hands it back with a result (event RETURNED)
+" or without one (the reserved leave event). Behaviour:
+" conformance/apps/README.md, section NAV.
+CLASS z2ui5_cl_conf_nav DEFINITION PUBLIC FINAL CREATE PUBLIC.
+
+  PUBLIC SECTION.
+    INTERFACES z2ui5_if_app.
+
+    DATA result  TYPE string.
+    DATA returns TYPE i.
+
+  PROTECTED SECTION.
+    DATA client TYPE REF TO z2ui5_if_client.
+
+    METHODS view_display.
+
+  PRIVATE SECTION.
+ENDCLASS.
+
+
+CLASS z2ui5_cl_conf_nav IMPLEMENTATION.
+
+  METHOD z2ui5_if_app~main.
+
+    me->client = client.
+
+    IF client->check_on_navigated( ).
+      " a return WITH a result arrives as the event RETURNED (named by the
+      " target's nav_app_leave) on a navigated roundtrip - read it, then render
+      IF client->get_event( ) = `RETURNED`.
+        DATA(lo_target) = CAST z2ui5_cl_conf_nav_tgt( client->get_app_prev( ) ).
+        result  = lo_target->output.
+        returns = returns + 1.
+      ENDIF.
+      view_display( ).
+    ELSEIF client->check_on_event( `CALL` ).
+      DATA(lo_call) = NEW z2ui5_cl_conf_nav_tgt( ).
+      lo_call->input = `from caller`.
+      client->nav_app_call( lo_call ).
+    ENDIF.
+
+  ENDMETHOD.
+
+  METHOD view_display.
+
+    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
+
+            )->ele( `Page`
+                )->a( n = `title` v = `conformance - nav caller`
+
+                )->tag( `Text`
+                    )->a( n = `id`   v = `result`
+                    )->a( n = `text` v = client->_bind( result )
+                )->tag( `Text`
+                    )->a( n = `id`   v = `returns`
+                    )->a( n = `text` v = client->_bind( returns )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Call`
+                    )->a( n = `press` v = client->_event( `CALL` ) ).
+
+    client->view_display( view->stringify( ) ).
+
+  ENDMETHOD.
+
+ENDCLASS.
diff --git a/conformance/apps/abap/z2ui5_cl_conf_nav.clas.xml b/conformance/apps/abap/z2ui5_cl_conf_nav.clas.xml
new file mode 100644
index 0000000..8bddbd4
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_nav.clas.xml
@@ -0,0 +1,16 @@
+
+
+ 
+  
+   
+    Z2UI5_CL_CONF_NAV
+    E
+    protocol conformance - navigation caller
+    1
+    X
+    X
+    X
+   
+  
+ 
+
diff --git a/conformance/apps/abap/z2ui5_cl_conf_nav_tgt.clas.abap b/conformance/apps/abap/z2ui5_cl_conf_nav_tgt.clas.abap
new file mode 100644
index 0000000..792ddcd
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_nav_tgt.clas.abap
@@ -0,0 +1,66 @@
+" Conformance app NAV_TGT - the called app of NAV. DONE leaves with a
+" result, the page's back button leaves through the reserved leave event.
+" Behaviour: conformance/apps/README.md, section NAV.
+CLASS z2ui5_cl_conf_nav_tgt DEFINITION PUBLIC FINAL CREATE PUBLIC.
+
+  PUBLIC SECTION.
+    INTERFACES z2ui5_if_app.
+
+    DATA input    TYPE string.
+    DATA output   TYPE string.
+    DATA has_prev TYPE abap_bool.
+
+  PROTECTED SECTION.
+    DATA client TYPE REF TO z2ui5_if_client.
+
+    METHODS view_display.
+
+  PRIVATE SECTION.
+ENDCLASS.
+
+
+CLASS z2ui5_cl_conf_nav_tgt IMPLEMENTATION.
+
+  METHOD z2ui5_if_app~main.
+
+    me->client = client.
+    IF client->check_on_navigated( ).
+      has_prev = client->check_app_prev_stack( ).
+      view_display( ).
+      RETURN.
+    ENDIF.
+
+    IF client->check_on_event( `DONE` ).
+      output = |{ input }!|.
+      client->nav_app_leave( event = `RETURNED` ).
+    ENDIF.
+
+  ENDMETHOD.
+
+  METHOD view_display.
+
+    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
+
+            )->ele( `Page`
+                )->a( n = `title`          v = `conformance - nav target`
+                )->a( n = `showNavButton`  b = has_prev
+                )->a( n = `navButtonPress` v = client->_event_nav_app_leave( )
+
+                )->tag( `Text`
+                    )->a( n = `id`   v = `input`
+                    )->a( n = `text` v = client->_bind( input )
+                )->tag( `Text`
+                    )->a( n = `id`   v = `output`
+                    )->a( n = `text` v = client->_bind( output )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Done`
+                    )->a( n = `press` v = client->_event( `DONE` ) ).
+
+    client->view_display( view->stringify( ) ).
+
+  ENDMETHOD.
+
+ENDCLASS.
diff --git a/conformance/apps/abap/z2ui5_cl_conf_nav_tgt.clas.xml b/conformance/apps/abap/z2ui5_cl_conf_nav_tgt.clas.xml
new file mode 100644
index 0000000..00024b8
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_nav_tgt.clas.xml
@@ -0,0 +1,16 @@
+
+
+ 
+  
+   
+    Z2UI5_CL_CONF_NAV_TGT
+    E
+    protocol conformance - navigation target
+    1
+    X
+    X
+    X
+   
+  
+ 
+
diff --git a/conformance/apps/abap/z2ui5_cl_conf_route.clas.abap b/conformance/apps/abap/z2ui5_cl_conf_route.clas.abap
new file mode 100644
index 0000000..af060f6
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_route.clas.abap
@@ -0,0 +1,70 @@
+" Conformance app ROUTE - hash routing: switches the KEEP mode on in its
+" first roundtrip, so every response carries the ROUTER intent, and calls
+" Z2UI5_CL_CONF_NAV_TGT to show the history push of a nav_app_call.
+" Behaviour: conformance/apps/README.md, section ROUTE.
+CLASS z2ui5_cl_conf_route DEFINITION PUBLIC FINAL CREATE PUBLIC.
+
+  PUBLIC SECTION.
+    INTERFACES z2ui5_if_app.
+
+    DATA count TYPE i.
+
+  PROTECTED SECTION.
+    DATA client TYPE REF TO z2ui5_if_client.
+
+    METHODS view_display.
+
+  PRIVATE SECTION.
+ENDCLASS.
+
+
+CLASS z2ui5_cl_conf_route IMPLEMENTATION.
+
+  METHOD z2ui5_if_app~main.
+
+    me->client = client.
+    IF client->check_on_init( ).
+      client->follow_up_action( val   = z2ui5_if_client=>cs_event-hash_routing
+                                t_arg = VALUE #( ( z2ui5_if_client=>cs_nav_mode-keep ) ) ).
+    ENDIF.
+    IF client->check_on_navigated( ).
+      view_display( ).
+      RETURN.
+    ENDIF.
+
+    CASE client->get_event( ).
+      WHEN `COUNT`.
+        count = count + 1.
+      WHEN `CALL`.
+        DATA(lo_call) = NEW z2ui5_cl_conf_nav_tgt( ).
+        lo_call->input = `from route`.
+        client->nav_app_call( lo_call ).
+    ENDCASE.
+
+  ENDMETHOD.
+
+  METHOD view_display.
+
+    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
+
+            )->ele( `Page`
+                )->a( n = `title` v = `conformance - route`
+
+                )->tag( `Text`
+                    )->a( n = `id`   v = `count`
+                    )->a( n = `text` v = client->_bind( count )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Count`
+                    )->a( n = `press` v = client->_event( `COUNT` )
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Call`
+                    )->a( n = `press` v = client->_event( `CALL` ) ).
+
+    client->view_display( view->stringify( ) ).
+
+  ENDMETHOD.
+
+ENDCLASS.
diff --git a/conformance/apps/abap/z2ui5_cl_conf_route.clas.xml b/conformance/apps/abap/z2ui5_cl_conf_route.clas.xml
new file mode 100644
index 0000000..8432b56
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_route.clas.xml
@@ -0,0 +1,16 @@
+
+
+ 
+  
+   
+    Z2UI5_CL_CONF_ROUTE
+    E
+    protocol conformance - hash routing
+    1
+    X
+    X
+    X
+   
+  
+ 
+
diff --git a/conformance/apps/abap/z2ui5_cl_conf_slots.clas.abap b/conformance/apps/abap/z2ui5_cl_conf_slots.clas.abap
new file mode 100644
index 0000000..15cc1fc
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_slots.clas.abap
@@ -0,0 +1,181 @@
+" Conformance app SLOTS - the four view slots besides MAIN: a popup, a
+" popover anchored to a button, and the two nested views, each opened and
+" closed by a backend event. Behaviour: conformance/apps/README.md,
+" section SLOTS.
+CLASS z2ui5_cl_conf_slots DEFINITION PUBLIC FINAL CREATE PUBLIC.
+
+  PUBLIC SECTION.
+    INTERFACES z2ui5_if_app.
+
+    DATA popup_text TYPE string.
+
+  PROTECTED SECTION.
+    DATA client TYPE REF TO z2ui5_if_client.
+
+    METHODS view_display.
+    METHODS on_event.
+    METHODS popup_display
+      IMPORTING
+        title TYPE string.
+    METHODS popover_display.
+    METHODS nest_display
+      IMPORTING
+        slot TYPE string.
+
+  PRIVATE SECTION.
+ENDCLASS.
+
+
+CLASS z2ui5_cl_conf_slots IMPLEMENTATION.
+
+  METHOD z2ui5_if_app~main.
+
+    me->client = client.
+    IF client->check_on_navigated( ).
+      view_display( ).
+    ELSEIF client->check_on_event( ).
+      on_event( ).
+    ENDIF.
+
+  ENDMETHOD.
+
+  METHOD view_display.
+
+    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` ).
+
+    DATA(page) = view->ele( `Page`
+        )->a( n = `title` v = `conformance - slots` ).
+
+    page->tag( `Button`
+        )->a( n = `id`    v = `btn_popup`
+        )->a( n = `text`  v = `Popup`
+        )->a( n = `press` v = client->_event( `POPUP_OPEN` )
+    )->tag( `Button`
+        )->a( n = `id`    v = `btn_popover`
+        )->a( n = `text`  v = `Popover`
+        )->a( n = `press` v = client->_event( `POPOVER_OPEN` )
+    )->tag( `Button`
+        )->a( n = `text`  v = `Nest`
+        )->a( n = `press` v = client->_event( `NEST_OPEN` )
+    )->tag( `Button`
+        )->a( n = `text`  v = `Nest2`
+        )->a( n = `press` v = client->_event( `NEST2_OPEN` )
+    )->tag( `VBox`
+        )->a( n = `id` v = `nest_anchor`
+    )->tag( `VBox`
+        )->a( n = `id` v = `nest2_anchor` ).
+
+    client->view_display( view->stringify( ) ).
+
+  ENDMETHOD.
+
+  METHOD on_event.
+
+    CASE client->get_event( ).
+      WHEN `POPUP_OPEN`.
+        popup_display( `conformance popup` ).
+      WHEN `POPUP_REPLACE`.
+        " two displays of one slot in one roundtrip - the last one counts
+        popup_display( `first` ).
+        popup_display( `second` ).
+      WHEN `POPUP_CLOSE`.
+        client->popup_destroy( ).
+      WHEN `MAIN_AND_POPUP`.
+        " called in the reverse of the slot order on purpose
+        popup_display( `with main` ).
+        view_display( ).
+      WHEN `POPOVER_OPEN`.
+        popover_display( ).
+      WHEN `POPOVER_CLOSE`.
+        client->popover_destroy( ).
+      WHEN `NEST_OPEN`.
+        nest_display( z2ui5_if_client=>cs_view-nested ).
+      WHEN `NEST_CLOSE`.
+        client->nest_view_destroy( ).
+      WHEN `NEST2_OPEN`.
+        nest_display( z2ui5_if_client=>cs_view-nested2 ).
+      WHEN `NEST2_CLOSE`.
+        client->nest2_view_destroy( ).
+    ENDCASE.
+
+  ENDMETHOD.
+
+  METHOD popup_display.
+
+    DATA(popup) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `FragmentDefinition` ns = `core`
+            )->a( n = `xmlns`      v = `sap.m`
+            )->a( n = `xmlns:core` v = `sap.ui.core` ).
+
+    popup->ele( `Dialog`
+        )->a( n = `title` v = title
+
+        )->tag( `Input`
+            )->a( n = `id`    v = `popup_text`
+            )->a( n = `value` v = client->_bind( popup_text )
+
+        )->ele( `buttons`
+
+            )->tag( `Button`
+                )->a( n = `text`  v = `Close`
+                )->a( n = `press` v = client->_event( `POPUP_CLOSE` )
+            )->tag( `Button`
+                )->a( n = `text`  v = `Close here`
+                )->a( n = `press` v = client->follow_up_action( z2ui5_if_client=>cs_event-popup_close ) ).
+
+    client->popup_display( popup->stringify( ) ).
+
+  ENDMETHOD.
+
+  METHOD popover_display.
+
+    DATA(popover) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `FragmentDefinition` ns = `core`
+            )->a( n = `xmlns`      v = `sap.m`
+            )->a( n = `xmlns:core` v = `sap.ui.core` ).
+
+    popover->ele( `Popover`
+        )->a( n = `title` v = `conformance popover`
+
+        )->tag( `Button`
+            )->a( n = `text`  v = `Close`
+            )->a( n = `press` v = client->_event( `POPOVER_CLOSE` ) ).
+
+    client->popover_display( xml   = popover->stringify( )
+                             by_id = `btn_popover` ).
+
+  ENDMETHOD.
+
+  METHOD nest_display.
+
+    DATA(nest) = z2ui5_cl_ui5_view_builder=>factory(
+        )->ele( n = `View` ns = `mvc`
+            )->a( n = `xmlns`     v = `sap.m`
+            )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
+
+            )->ele( `VBox`
+
+                )->tag( `Text`
+                    )->a( n = `text` t = |conformance { slot }|
+                )->tag( `Button`
+                    )->a( n = `text`  v = `Close`
+                    )->a( n = `press` v = client->_event( |{ slot }_CLOSE| ) ).
+
+    IF slot = z2ui5_if_client=>cs_view-nested.
+      client->nest_view_display( val            = nest->stringify( )
+                                 id             = `nest_anchor`
+                                 method_insert  = `addItem`
+                                 method_destroy = `removeAllItems` ).
+    ELSE.
+      client->nest2_view_display( val            = nest->stringify( )
+                                  id             = `nest2_anchor`
+                                  method_insert  = `addItem`
+                                  method_destroy = `removeAllItems` ).
+    ENDIF.
+
+  ENDMETHOD.
+
+ENDCLASS.
diff --git a/conformance/apps/abap/z2ui5_cl_conf_slots.clas.xml b/conformance/apps/abap/z2ui5_cl_conf_slots.clas.xml
new file mode 100644
index 0000000..369d262
--- /dev/null
+++ b/conformance/apps/abap/z2ui5_cl_conf_slots.clas.xml
@@ -0,0 +1,16 @@
+
+
+ 
+  
+   
+    Z2UI5_CL_CONF_SLOTS
+    E
+    protocol conformance - popup, popover, nested views
+    1
+    X
+    X
+    X
+   
+  
+ 
+
diff --git a/conformance/apps/abaplint.jsonc b/conformance/apps/abaplint.jsonc
new file mode 100644
index 0000000..9ee1355
--- /dev/null
+++ b/conformance/apps/abaplint.jsonc
@@ -0,0 +1,388 @@
+// abaplint configuration of the ABAP conformance apps (conformance/apps/abap).
+//
+// The rule block is abap2UI5's shared app rule set,
+// abap2UI5/abap2UI5 .github/abaplint/app-rules.json (version 1), copied
+// unchanged - except object_naming, which encodes this repository's prefix
+// (Z2UI5_CL_CONF_). The framework dependency is the downported abap2UI5 of
+// the @abap2ui5/node-runtime release the suite is run against (npm ci first),
+// so the apps are checked against exactly the framework they are transpiled
+// with. Run: npm run lint:abap
+{
+  "global": {
+    "files": "/abap/**/*.*"
+  },
+  "dependencies": [
+    {
+      "url": "https://github.com/abapedia/steampunk-2305-api-intersect-702",
+      "folder": "/deps/steampunk",
+      "files": "/src/**/*.*"
+    },
+    {
+      "folder": "/../../node_modules/@abap2ui5/node-runtime/downport",
+      "files": "/**/*.*"
+    }
+  ],
+  "syntax": {
+    "version": "v750",
+    "errorNamespace": "."
+  },
+  "rules": {
+    "7bit_ascii": {
+      "exclude": [
+        "z2ui5_cl_smpc_app_(012|040|092|103|104|114|134|185|191|194|215|218|254|290|302|303|350|356|357|358|360|445|525|558|565|572|575|578|579|580|584|611)\\.clas\\.",
+        "z2ui5_cl_smpc_app_000\\.clas\\.",
+        "z2ui5_cl_smpc_demo_\\d+\\.clas\\."
+      ]
+    },
+    "abapdoc": false,
+    "add_test_attributes": true,
+    "aff_and_xml": true,
+    "align_parameters": false,
+    "align_pseudo_comments": true,
+    "align_type_expressions": true,
+    "allowed_object_naming": true,
+    "allowed_object_types": {
+      "allowed": [
+        "CLAS",
+        "DEVC",
+        "INTF",
+        "TABL",
+        "DTEL",
+        "DDLS",
+        "BDEF",
+        "SRVD",
+        "SRVB",
+        "SICF",
+        "SAPC",
+        "SAMC",
+        "SUSH",
+        "SMIM",
+        "G4BA"
+      ]
+    },
+    "ambiguous_statement": true,
+    "avoid_use": {
+      "skipQuickFix": false,
+      "define": true,
+      "statics": true,
+      "defaultKey": true,
+      "break": true,
+      "testSeams": true,
+      "describeLines": true,
+      "exportToMemory": true,
+      "exportToDatabase": true
+    },
+    "begin_end_names": true,
+    "begin_single_include": true,
+    "call_transaction_authority_check": true,
+    "catch_and_raise": true,
+    "cds_association_name": true,
+    "cds_comment_style": true,
+    "cds_field_order": true,
+    "cds_legacy_view": true,
+    "cds_naming": {
+      "basicInterfaceView": "Z2UI5_",
+      "compositeInterfaceView": "Z2UI5_",
+      "consumptionView": "Z2UI5_",
+      "basicRestrictedReuseView": "Z2UI5_",
+      "compositeRestrictedReuseView": "Z2UI5_",
+      "privateView": "Z2UI5_",
+      "remoteAPIView": "Z2UI5_",
+      "viewExtend": "Z2UI5_",
+      "extensionIncludeView": "Z2UI5_",
+      "derivationFunction": "Z2UI5_",
+      "abstractEntity": "Z2UI5_"
+    },
+    "cds_parser_error": true,
+    "chain_mainly_declarations": true,
+    "change_if_to_case": true,
+    "check_abstract": {
+      "exclude": [
+        "z2ui5_cl_smps_bp_[a-z]+\\.clas\\."
+      ]
+    },
+    "check_comments": true,
+    "check_ddic": true,
+    "check_include": true,
+    "check_subrc": {
+      "openDataset": true,
+      "authorityCheck": true,
+      "selectSingle": false,
+      "selectTable": false,
+      "updateDatabase": true,
+      "insertDatabase": true,
+      "modifyDatabase": true,
+      "deleteDatabase": true,
+      "readTable": true,
+      "assign": true,
+      "find": true
+    },
+    "check_syntax": {
+      "exclude": [
+        "z2ui5_cl_smps_app_489(_ws)?\\.clas\\."
+      ]
+    },
+    "check_text_elements": true,
+    "check_transformation_exists": true,
+    "class_attribute_names": false,
+    "classic_exceptions_overlap": true,
+    "clear_exporting_parameters": true,
+    "cloud_types": true,
+    "colon_missing_space": true,
+    "commented_code": true,
+    "constant_classes": true,
+    "constructor_visibility_public": true,
+    "contains_tab": true,
+    "cyclic_oo": true,
+    "cyclomatic_complexity": true,
+    "dangerous_statement": {
+      "execSQL": true,
+      "kernelCall": true,
+      "systemCall": true,
+      "insertReport": true,
+      "generateDynpro": true,
+      "generateReport": true,
+      "generateSubroutine": true,
+      "deleteReport": true,
+      "deleteTextpool": true,
+      "insertTextpool": true,
+      "deleteDynpro": true,
+      "exportDynpro": true,
+      "editorCallForReport": true,
+      "dynamicSQL": false,
+      "ignoreRAPQueryProvider": true
+    },
+    "db_operation_in_loop": true,
+    "definitions_top": true,
+    "description_empty": true,
+    "double_space": {
+      "keywords": false,
+      "startParen": true,
+      "endParen": false,
+      "afterColon": true
+    },
+    "downport": true,
+    "dynpro_checks": true,
+    "easy_to_find_messages": true,
+    "empty_event": true,
+    "empty_line_in_statement": false,
+    "empty_statement": true,
+    "empty_structure": {
+      "loop": true,
+      "loopAllowIfSubrc": true,
+      "if": true,
+      "while": true,
+      "case": true,
+      "select": true,
+      "do": true,
+      "at": true,
+      "try": true,
+      "when": false
+    },
+    "exit_or_check": true,
+    "expand_macros": true,
+    "exporting": true,
+    "fm_global_parameters_obsolete": true,
+    "forbidden_identifier": true,
+    "forbidden_pseudo_and_pragma": true,
+    "forbidden_void_type": true,
+    "form_tables_obsolete": true,
+    "fully_type_constants": {
+      "checkData": true,
+      "exclude": [
+        "z2ui5_cl_smps_app_(007|010)\\.clas\\."
+      ]
+    },
+    "fully_type_itabs": true,
+    "function_module_recommendations": true,
+    "functional_writing": true,
+    "global_class": true,
+    "identical_conditions": true,
+    "identical_contents": true,
+    "identical_descriptions": true,
+    "identical_form_names": true,
+    "identical_move": true,
+    "if_in_if": true,
+    "implement_methods": true,
+    "implicit_start_of_selection": true,
+    "in_statement_indentation": false,
+    "indentation": false,
+    "index_completely_contained": true,
+    "inline_data_old_versions": true,
+    "intf_referencing_clas": true,
+    "invalid_table_index": true,
+    "keep_single_parameter_on_one_line": false,
+    "keyword_case": {
+      "style": "upper",
+      "ignoreExceptions": true,
+      "ignoreLowerClassImplmentationStatement": true,
+      "exclude": [
+        "z2ui5_cl_smps_"
+      ],
+      "ignoreKeywords": []
+    },
+    "line_break_multiple_parameters": false,
+    "line_break_style": true,
+    "line_length": {
+      "length": 255
+    },
+    "line_only_punc": false,
+    "local_class_naming": {
+      "exclude": [
+        "z2ui5_cl_smps_bp_[a-z]+\\.clas\\.locals_imp\\."
+      ],
+      "patternKind": "required",
+      "local": "^LCL_.+$",
+      "exception": "^LCX_.+$",
+      "test": "^LTCL_.+$"
+    },
+    "local_testclass_consistency": true,
+    "local_variable_names": false,
+    "macro_naming": true,
+    "main_file_contents": true,
+    "many_parentheses": true,
+    "max_one_method_parameter_per_line": true,
+    "max_one_statement": {
+      "exclude": [
+        "z2ui5_cl_smps_app_319\\.clas\\."
+      ]
+    },
+    "message_exists": true,
+    "method_implemented_twice": true,
+    "method_length": {
+      "statements": 100,
+      "errorWhenEmpty": false,
+      "ignoreTestClasses": true,
+      "checkForms": true,
+      "checkFunctionModules": true,
+      "exclude": [
+        "z2ui5_cl_smpc_app_000\\.clas\\."
+      ]
+    },
+    "method_overwrites_builtin": true,
+    "method_parameter_names": false,
+    "mix_returning": true,
+    "modify_only_own_db_tables": true,
+    "msag_consistency": true,
+    "names_no_dash": true,
+    "nesting": true,
+    "newline_between_methods": true,
+    "no_aliases": true,
+    "no_chained_assignment": true,
+    "no_comments_between_methods": true,
+    "no_exclamation_escape": true,
+    "no_external_form_calls": true,
+    "no_inline_in_optional_branches": false,
+    "no_macros": true,
+    "no_mandt_in_database_operations": true,
+    "no_prefixes": false,
+    "no_public_attributes": false,
+    "no_yoda_conditions": {
+      "onlyConstants": true
+    },
+    "nrob_consistency": true,
+    "obsolete_statement": true,
+    "omit_parameter_name": true,
+    "omit_preceding_zeros": true,
+    "omit_receiving": true,
+    "parser_702_chaining": true,
+    "parser_bad_exceptions": true,
+    "parser_error": true,
+    "parser_missing_space": true,
+    "pragma_style": true,
+    "prefer_corresponding": false,
+    "prefer_inline": false,
+    "prefer_is_not": true,
+    "prefer_pragmas": true,
+    "prefer_raise_exception_new": {
+      "exclude": [
+        "z2ui5_cl_smps_app_485\\.clas\\."
+      ]
+    },
+    "prefer_returning_to_exporting": true,
+    "prefer_xsdbool": true,
+    "preferred_compare_operator": true,
+    "prefix_is_current_class": true,
+    "reduce_procedural_code": true,
+    "reduce_string_templates": true,
+    "redundant_conversion": true,
+    "release_idoc": true,
+    "remove_descriptions": true,
+    "rfc_error_handling": true,
+    "select_add_order_by": true,
+    "select_performance": true,
+    "select_single_full_key": {
+      "allowPseudo": true,
+      "exclude": [
+        "z2ui5_cl_smps_bp_[a-z]+\\.clas\\.locals_imp\\."
+      ]
+    },
+    "selection_screen_naming": true,
+    "selection_screen_texts_missing": true,
+    "sequential_blank": {
+      "lines": 3
+    },
+    "short_case": true,
+    "sicf_consistency": true,
+    "slow_parameter_passing": true,
+    "smim_consistency": false,
+    "space_before_colon": true,
+    "space_before_dot": true,
+    "sql_escape_host_variables": true,
+    "sql_value_conversion": true,
+    "start_at_tab": true,
+    "static_call_via_instance": true,
+    "strict_sql": true,
+    "superclass_final": {
+      "exclude": [
+        "z2ui5_cl_smps_app_489_ws\\.clas\\."
+      ]
+    },
+    "superfluous_value": true,
+    "sy_modification": true,
+    "sy_read_restriction": true,
+    "tabl_enhancement_category": true,
+    "tables_declared_locally": true,
+    "try_without_catch": true,
+    "type_form_parameters": true,
+    "types_naming": {
+      "pattern": "^TY_"
+    },
+    "uncaught_exception": true,
+    "unknown_types": true,
+    "unnecessary_chaining": true,
+    "unnecessary_pragma": true,
+    "unnecessary_return": true,
+    "unreachable_code": true,
+    "unsecure_fae": true,
+    "unused_ddic": {
+      "exclude": [
+        "z2ui5_(t|d)_smps_"
+      ]
+    },
+    "unused_macros": true,
+    "unused_methods": {
+      "exclude": [
+        "z2ui5_cl_smps_bp_[a-z]+\\.clas\\.locals_imp\\."
+      ]
+    },
+    "unused_types": true,
+    "unused_variables": {
+      "exclude": [
+        "z2ui5_cl_smps_bp_[a-z]+\\.clas\\.locals_imp\\."
+      ]
+    },
+    "use_bool_expression": true,
+    "use_class_based_exceptions": true,
+    "use_line_exists": true,
+    "use_new": true,
+    "when_others_last": true,
+    "whitespace_end": true,
+    "xml_consistency": true,
+    "object_naming": {
+      "patternKind": "required",
+      "severity": "Error",
+      "clas": "^(?=.{1,25}$)Z2UI5_CL_CONF_"
+    }
+  }
+}
diff --git a/conformance/apps/cap2ui5/actions.js b/conformance/apps/cap2ui5/actions.js
new file mode 100644
index 0000000..2541e7c
--- /dev/null
+++ b/conformance/apps/cap2ui5/actions.js
@@ -0,0 +1,41 @@
+// Conformance app ACTIONS - the cap2UI5 equivalent of
+// conformance/apps/abap/z2ui5_cl_conf_actions.clas.abap.
+// Behaviour: conformance/apps/README.md, section ACTIONS.
+const { defineApp } = require("@cap2ui5/cds-plugin");
+
+defineApp("Z2UI5_CL_CONF_ACTIONS", class {
+  ticks = 0;
+
+  main(client) {
+    if (client.check_on_navigated()) {
+      client.view_display(
+        `` +
+        `` +
+        `` +
+        `