pattern_lint | node scripts/pattern-lint.mjs | a known-bad pattern reappears (each rule encodes a distilled §10 lesson; known open findings live in the script's BASELINE (currently empty — a stale entry fails) and in STATUS.md). Corpus-policy rules only since 2026-08-04: the generic half (popover-display-val, uncurated-formatter, hardcoded-binding-path, obsolete-bind-edit, event-arg-bare-brace, brace-in-style, empty view slot, numeric-bound-as-string, relative-bind-on-root-field, duplicate-for-iterator, private-mproperties → ui5-internal-access, commercial-ui5-host) moved into the linter and is gated via view_gates — do not re-add one here |
check_pins | node scripts/check-pins.mjs | the abap2UI5 pin policy is violated: A2UI5_PIN is missing or not a single well-formed 40-hex commit SHA (the ancestor-of-main check needs the network and stays with bump-a2ui5.yaml), OR an abaplint config's abap2UI5 dependency entry carries a "branch" key — only the pin governs the framework version. One allowlisted exception: .github/abaplint/abap_702.jsonc needs "branch": "702" (the 702 build resolves against the framework's downported branch). Duplicate "branch" keys in one entry always fail — JSON parsing takes the last, so a stale key next to the intended one is invisible to every consumer (a deleted-branch leftover sat exactly there, the journal 2026-08-10). A deliberate temporary feature-branch re-point must edit ALLOWED_BRANCHES in the script in the same change, so the temporary state shows in the diff and cannot survive a merge unseen |
structural_diff | node scripts/structural-diff.mjs --strict | a port's rendered view deviates from the original view.xml — control multiset, attribute names or simple binding values — without a declared deviation. Controls are flagged in BOTH directions: a control the original has that the port dropped (control missing) and a control the port ADDS that the original lacks (control extra — e.g. an inlined controller-built popup Dialog, or the controls of a bound row-template the original built imperatively). Only attributes are one-directional: it flags a missing attr but extra attrs the port adds are never flagged. So "the port only adds, never drops" is not a free pass — name every extra control in a deviation too. It compares the full qualified name incl. namespace prefix, so use the original view's prefix on controls (not a neighbour's convention). The original root's controllerName is always dropped, undeclared — it sits in the diff's IGNORED_ATTRS, like all xmlns:*. A diff is "declared" when some deviation's what text contains that control/attr name verbatim — so when you inline a controller-built popup, list the added Dialog/Button/… ; when you replace an imperative handler with a binding (dropping press/change), name that attribute — or it stays UNDECLARED. Exception: helper-built (dynamic) ports skip control-count diffs entirely (loop/helper-built counts can't match statically), which is one reason a helper-dedup'd view needs a render_smoke skip. A sidecar "structural_diff": { "skip": true, "reason": "…" } opts a deliberate breadth/capability probe out — and since 2026-08-04 that skip is re-verified per run exactly like render_smoke.skip: the diff is still computed, and a skip whose port shows no difference any more is STALE and fails (app 114 was the first catch) |
view_gates | node scripts/view-gates.mjs --strict (npm run view-gates) | one of the three view gates fails for a port. They all live in abap2UI5-linter now — this repo keeps only the corpus policy on top of it (which ports, POST_171 deviations, declared skips). (a) properties: a control or member the port writes does not exist, carries a value UI5 rejects, is deprecated at 1.71, or was introduced after 1.71 without a POST_171 deviation naming it — attributes, aggregations, event parameters read via ${$parameters>/<name>} in a t_arg (resolved per event), and the control itself (which the old member-level gate never saw). (b) structure: the builder call tree is malformed — an aggregation nested directly inside another aggregation (a missing )->end(, e.g. <footer> landing inside <columns>), a shut with no open to close, an attribute written twice on one control (z2ui5_cl_ui5_view_builder asserts on that). (c) render: the reconstructed view fails a real headless XMLView.create against the OpenUI5 runtime (invalid XML, unknown control/property, strict property-type violation, broken expression binding). Helper-method-built views (a captured node handle passed into a builder-returning helper and chained out — app 049) are reconstructed by the handle-aware path. A port the reconstructor cannot rebuild must declare "render_smoke": { "skip": true, "reason": "…" } in its sidecar — an undeclared non-reconstructable port FAILS. The same declared skip also covers a reconstructable port whose view genuinely does not render in the static headless harness (an sap.ui.integration Card needs an external manifest). Every skip is verified against the actual render each run: honoured only while the view still errors, and the moment it renders clean the declaration is stale and FAILS — so the skip set can never drift. Accessibility findings, unhandled events and other hint-severity findings are printed as advisories (·) and do not gate per finding — but the advisory ratchet (ADVISORY_BUDGET in scripts/view-gates.mjs, full runs only) pins their per-type counts: a change that ADDS advisory debt fails --strict, a change that removes some prints the number the budget can ratchet down to. Since the 2026-08-04 linter bump the gating rule set also includes , , , , , , (the app-043 BINDING_ERROR class), and (all measured 0 on the ports before shipping), plus in — an enum value newer than 1.71 is excusable by a POST_171 deviation naming it, exactly like a member (app 028's frameType declarations were confirmed by the rule's first run). A full run with the render gate on also writes the two README badges into (corpus reach, and the gate verdict as ) — from the gate's own verdict, not the raw findings, so a POST_171-excused finding or a declared skip never reddens a badge over a green gate; and deliberately do not write them, and commits them onto the PR branch. (live since the move to 0.1.0 from npm): is the linter's split of for an aggregation — an error rather than a warning, because UI5 resolves the unknown lowercase tag as a control class and the 404 kills the whole view instead of dropping one attribute. It is a version finding like any other, so a POST_171 deviation naming the aggregation still excuses it; 24 ports depend on that. An finding carries neither control nor member — only the glyph name in — which is why reads for the types, or no icon could ever be declared. |
e2e_smoke (heavy, on-demand) | npm run e2e:build then npm run e2e (scripts/e2e-build.mjs + e2e-smoke.mjs) | a port fails to run as the real app: it starts the transpiled abap2UI5 backend (framework + all ports via ?app_start=<class>, needs an abap2UI5 checkout — A2UI5_HOME), boots each port in headless Chromium (UI5 served from the local @openui5 packages), and fails if it does not boot+render or if a backend request 4xx/5xx or a JS exception fires. Unlike render_smoke (static view reconstruction) this exercises the actual backend roundtrip, Component boot and event wiring. Not in the fast gate set (multi-minute transpile + browser); run before a release or when touching the framework wire/runtime. The per-port interaction modules (meta/interactions/<class>.mjs, loaded by the driver; coverage catalogue in that directory's README) add real click→assert checks (e.g. 005 press→client-composed toast) — they are the systematic close path for LIVE_TEST deviations: after a green run, node scripts/close-live-tests.mjs --close <nums> converts them to NOTEs (the deviation text stays verbatim so gate declarations keep matching). A red nightly opens/updates a e2e nightly is red issue (e2e-nightly.yaml) instead of sitting in the Actions tab |
data_fidelity | node scripts/data-fidelity.mjs | (a) assets: a port seeds an asset value the sample's own archived files/mocks never mention (basename lookup over ui5/<lib>/<Name>/ + the ui5/mock/*.json the sample references by file name or top-level collection key), a full asset path whose folder differs from every mock occurrence, or an asset on a non-OpenUI5 UI5 host. (b) table values: every VALUE #( … ) block that inlines a mock array (matched to its ONE best array by ≥ 3 shared field names, ≥ 2 rows; sample-local JSON beats the shared mock on a name collision — app 010 carries its own modified products.json) is compared string-field-wise: equal row counts → positional row/field equality (a field the mock row omits is skipped — the port seeds the UI5 default there); fewer rows → per-field set membership, so a legitimate /Coll/0..n subset passes but an invented value fails. Values compare modulo the sanctioned https://sdk.openui5.org/ host-absolutization; numbers are not compared (formatting freedom). This is the deterministic form of the data audit — the wrong-neighbour-copy class (apps 162/142/119) that structural-diff (ignores values) and the render gate (mocks the model) can never see. Escapes: a deviation whose what names the basename/field/value verbatim, or a sidecar "data_fidelity": { "skip": true, "reason": "…" }. --report prints per port the mock string values missing from the ABAP source as the residual audit worksheet |
meta_valid | validate-meta.mjs + regenerate overview, coverage, status, SAMPLES.md & catalogue.json, git diff --exit-code -- src README.md api.md STATUS.md SAMPLES.md catalogue.json | an invalid sidecar, or a change forgot to regenerate the overview app / coverage docs / STATUS state block / the SAMPLES.md catalogue / the machine-readable catalogue.json |
chain_format | node scripts/chain-format.mjs (npm run check:chains) | a view chain breaks the layout rules - one call per line, four spaces per tree level, the end( ) column, the blank lines that carry the structure. npm run fmt:chains rewrites what it reports. The script is byte-identical in abap2UI5, samples and samples-stack; the full rule set is in the view-chain-layout guide |
prose_names | node scripts/check-prose-names.mjs (npm run check:prose) | a z2ui5_cl_* class named in README/AGENTS/CLAUDE/CONTRIBUTING/TRAINING/STATUS does not exist - here, or in the sibling repository whose prefix it carries (resolved through that repo's generated SAMPLES.md, framework names against an abap2UI5 checkout). A name that is meant to be absent goes into scripts/prose-absent.json with its reason; a blank reason fails the run. When a sibling is unreachable the run says which names it could not check and passes |