Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are.
- learn(bug-logs): **`BUG-DRAFT-grant-association-generalization-member`** — a member association owned by another module's entity cannot be named in a `grant` statement, so `revoke` + `re-grant` silently drops that member access and `mx check` reports CE0066; patch-only workaround provided. Found fixing a guest-groups association in a Mendix app (mxcli v0.23.0, Mendix 11.12.2). — MendixMau
- chore(inbox): **triage of `contrib/inbox/`: removed 15 promoted/obsolete files, gave the rest neutral names and scrubbed identifiers.** Gone: the ten machine-diff `*-patches.md` files from 2026-09-14, the 2026-09-27 field-project patch file, the built company-brain idea, and three already-promoted or superseded bug dumps. Kept for now: `2026-09-27-mxcli-upstream-issues.md`, because five drafts under `bug-logs/pending-github-issues/` cite it as their source. Four remaining files were renamed to `field-project-{a,b,c}` names; project and app names, home-style paths and personal names inside the 17 remaining entries were replaced with neutral labels or placeholders (technical content unchanged). — MendixMau
- learn(learned-mdl-preflight, learned-detection-gaps, microflow-preflight, testing-shape, learned-mdl-cannot-express, learned-page-patterns, rest-integration-first-time-right, journey-proof, learned-datagrid-customcontent-binding): **the skills side of BUG-142 to BUG-154.** STOP row 12 rewritten shorter for v0.24.0 — `ContentParams` takes a bare attribute or a quoted literal only, and the `toString(Attr)` fix it taught no longer builds; row 14 notes a narrowing `grant` merges, row 23 that `diff` prints false `-`/`~` lines. Detection gaps: the ContentParams row now covers any object-bound `dynamictext` and says check passes `if … then … else`; reserved words cover entities; new CE1870 row. microflow-preflight: CONV013 false positive and doc-comment round-trip resolved from "Not verified"; error handler without `return` is the second merge-overlap shape. testing-shape: `--test-endpoint` writes into the working model until a clean stop; `run --local` starts with scheduled events off (new register row). Also: five cannot-express rows (tab badge, validate-while-typing, AppTitle, always-open nav groups, sidebar toggle / page-load / Marketplace layout) and the DG2 `emptyplaceholder` correction; control-bar attributes, nested list view `Editable` and REPLACE-not-re-run-safe in page patterns; v0.24.0 REST bullets; two OQL journey traps. Triages three inbox notes (ContentParams, REST/JSON/page field notes, scheduled events) — card-disbursement requirements-driven build
- fix(project-bin/check-design-reaches-app.sh): **the model directory now comes from `find_model_dir`, and a knob file it cannot read is a fault, never clean.** #162 added a one-level `app/*.mpr` probe; this replaces it with `find_model_dir` in `_common.sh` (`$PROJECT_ROOT`, `$MPR_FILE`, the `app/` probe), plus an `app/theme` / `app/deployment` probe when no `.mpr` is found. The fault is the part #162 did not cover: a missing knob file, or one that declares no knobs, printed "knobs bound 0 of 0" under `clean` with exit 0; it now reports `FAULT … UNMEASURED` and exits 2. The knob file can also be passed as a third argument. Fixture: `tests/wave2/test-design-reaches-app.sh` cases 9–10 (both layouts, with and without an `.mpr`, missing and empty knob file, overrides). Field run: a Stage 5 obligation pass on a two-tree Mendix 11.14 app, 0 of 0 before, 26 of 30 knobs and 157 of 157 tokens after, with no arguments — a two-tree greenfield build
- learn(bug-logs): **thirteen new ledger entries (BUG-142 to BUG-154) and five retests from mxcli v0.24.0 / Mendix 11.13.0.** New: `ContentParams` builds only from a bare attribute or a quoted literal, and `if … then … else` passes check and fails CE1613 (supersedes the BUG-23 fix); theme seeder and Atlas-map gaps; `create user role` omits `CheckSecurity`/`GUID`; a narrowing `grant` merges; `diff` false modifications; check/exec disagreeing on same-script documents; E007 on a last quoted enum literal; a table of check/lint misfires and check-green/mxbuild-red constructs; DG2 and filter definition gaps; `rest call … body $Var` sends the literal text; published-REST, mapping and Java action gaps; OQL `HAVING` and date aggregates; MDL surface gaps. Retests: BUG-08 (REPLACE still collides, and on reused child names), BUG-77 reproduced with its trigger isolated, BUG-92 and BUG-117 field instances, BUG-122 on a snippet list view; the layout-merge draft gains the error-handler shape. Each from a `check`/exec/mxbuild A/B or a BSON read on a scratch copy — card-disbursement requirements-driven build
- learn(learned-dg2-patterns, learned-css-that-never-applied, testing-shape, learned-db-assertions, module-review): **five UI and test-harness notes.** DG2 column sizing at phone width: shrink only the wrapper around text that may ellipsize and floor ids/pills/headers at `max-content` (a blanket `min-width: 0` spilled 20 of 20 pills at 390px); Atlas base properties with no knob (`.btn` font-weight, `.form-group` row layout above 767px) survive a port that trusts the knobs; a gate build while `run --local` serves drops `dist/` and hangs every login (runtime.log shows the 404); `mx.data.get` is disabled in the React client, so data probes go through `mxcli oql --direct`; measure text alignment on glyph ranges, not element boxes — card-disbursement requirements-driven build
- learn(learned-detection-gaps, learned-microflow-patterns, learned-workflow-patterns): **four new detection-gap rows and one closed gap, from mxcli v0.24.0 / Mendix 11.13.0.** An `if` with no `else` inside `on error { }` passes check and fails mxbuild (CE0079/CE0773); a Java action parameter named with a Mendix-reserved word (`Case`) is CE7247 at mxbuild only; a data widget on a page a no-read role can open passed a scratch `mx check` and failed exec's gate with CE2729; `[%CurrentUser%]` in a microflow a REST or scheduled path reaches passes every static rung and every UI journey, then fails in the system session (look the account up by login instead — pattern added). CE7410 (a task page without `System.WorkflowUserTask`) is now refused at `check --references`. The `calculated by` row no longer tells you to verify with `mx.data.get`, which the React client disables — card-disbursement requirements-driven build
Expand Down
104 changes: 76 additions & 28 deletions project-bin/check-design-reaches-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,21 @@
# styling nothing.
#
# project-bin/check-design-reaches-app.sh # defaults, from the project root
# project-bin/check-design-reaches-app.sh <design.css> <built.css>
# project-bin/check-design-reaches-app.sh <design.css> <built.css> [custom-variables.scss]
#
# Exit 0 clean, 1 violations, 2 inspected nothing (NOT a pass).
# Exit 0 clean, 1 violations, 2 inspected nothing or a pass could not run (NOT a pass).
#
# ── BOTH LAYOUTS (2026-09-28) ───────────────────────────────────────────────────────────────
#
# The theme, themesource and deployment trees sit beside the .mpr, NOT at the project root.
# On a single-tree checkout those are the same directory; on a two-tree checkout (repo at the
# root, the app under app/) they are not. This script used to read theme/web/custom-variables.scss
# relative to the working directory, found nothing on a two-tree checkout, and printed
# "knobs bound 0 of 0" under "clean, 1 warning" — a false clean on a Mendix 11.14 app whose
# real figure, with the path given by hand, was 26 of 30 knobs bound and 157 of 157 tokens.
# The model directory is now resolved the way every other project-bin script resolves it
# (find_model_dir in _common.sh: $PROJECT_ROOT, $MPR_FILE, the app/ probe), and a knob file
# that cannot be found, or that declares no knobs, is an instrument fault — exit 2, never clean.
#
# THIS CHECK REQUIRES A BUILD. That is the point: there is no way to answer "did it reach the
# app" from source alone, which is exactly why nothing answered it. With no built stylesheet it
Expand All @@ -84,38 +96,56 @@ set -uo pipefail
DESIGN="${1:-}"
BUILT="${2:-}"

# Where the model lives. The design system and mdlsource/ sit at the repo root either way; the
# built sheet, theme/ and themesource/ sit beside the .mpr — the root on a single-tree checkout,
# app/ on a two-tree one (repo at the root, `mxcli new` app under app/, the same one-level probe
# _common.sh's find_mpr makes). Root-relative defaults alone exited 2 on every two-tree project
# (field run, card-disbursement requirements-driven build, 2026-09-25).
MP=""
if ! ls ./*.mpr >/dev/null 2>&1 && ls app/*.mpr >/dev/null 2>&1; then MP="app/"; fi
# ── Where things live ───────────────────────────────────────────────────────────────────────
# ROOT holds design/ and mdlsource/; MODEL_DIR holds the .mpr and, beside it, theme/,
# themesource/ and deployment/. The script has always run from the project root, so ROOT is
# $PROJECT_ROOT when set, else the working directory — not _common.sh's own tier-2 guess, which
# for the toolkit's shared copy is the toolkit. MODEL_DIR comes from find_model_dir when an .mpr
# can be found; a copy installed beside an older _common.sh, or a tree with no .mpr, falls back
# to probing app/ for the framework's trees.
ROOT="${PROJECT_ROOT:-$(pwd)}"
MODEL_DIR=""
if [ -f "$(dirname "${BASH_SOURCE[0]}")/_common.sh" ]; then
PROJECT_ROOT="$ROOT"
. "$(dirname "${BASH_SOURCE[0]}")/_common.sh"
if type find_model_dir >/dev/null 2>&1; then
MODEL_DIR="$(find_model_dir 2>/dev/null)" || MODEL_DIR=""
fi
fi
if [ -z "$MODEL_DIR" ]; then
MODEL_DIR="$ROOT"
if [ ! -d "$ROOT/theme" ] && [ ! -d "$ROOT/deployment" ] && { [ -d "$ROOT/app/theme" ] || [ -d "$ROOT/app/deployment" ]; }; then
MODEL_DIR="$ROOT/app"
fi
fi
# Paths print relative to where the user stands when they can; absolute otherwise.
rel() { case "$1" in "$(pwd)"/*) printf '%s' "${1#"$(pwd)"/}" ;; *) printf '%s' "$1" ;; esac; }

if [ -z "$DESIGN" ]; then
for c in design/ds.css design/design-system.css; do
[ -f "$c" ] && { DESIGN="$c"; break; }
for c in "$ROOT/design/ds.css" "$ROOT/design/design-system.css" "$MODEL_DIR/design/ds.css" "$MODEL_DIR/design/design-system.css"; do
[ -f "$c" ] && { DESIGN="$(rel "$c")"; break; }
done
fi
if [ -z "$BUILT" ]; then
for c in "${MP}deployment/web/theme.compiled.css" "${MP}deployment/web/theme.css"; do
[ -f "$c" ] && { BUILT="$c"; break; }
for c in "$MODEL_DIR/deployment/web/theme.compiled.css" "$MODEL_DIR/deployment/web/theme.css"; do
[ -f "$c" ] && { BUILT="$(rel "$c")"; break; }
done
fi

# The framework's own customization surface. Atlas has called this file the same thing since
# Atlas 2; a project that renamed it passes it as a third argument via CUSTOM_VARS.
CUSTOM_VARS="${CUSTOM_VARS:-${MP}theme/web/custom-variables.scss}"
# Atlas 2; a project that renamed it passes it as the third argument, or via CUSTOM_VARS.
CUSTOM_VARS="${3:-${CUSTOM_VARS:-$(rel "$MODEL_DIR/theme/web/custom-variables.scss")}}"
THEME_WEB="$MODEL_DIR/theme/web"

if [ -z "$DESIGN" ] || [ ! -f "$DESIGN" ]; then
printf 'check-design-reaches-app: no design system stylesheet found.\n' >&2
printf ' Searched: design/ds.css, design/design-system.css\n' >&2
printf ' Searched: design/ds.css, design/design-system.css under %s and %s\n' "$ROOT" "$MODEL_DIR" >&2
printf ' This is NOT a pass. Name it explicitly, or run from the project root.\n' >&2
exit 2
fi
if [ -z "$BUILT" ] || [ ! -f "$BUILT" ]; then
printf 'check-design-reaches-app: no BUILT stylesheet found.\n' >&2
printf ' Searched: %sdeployment/web/theme.compiled.css, %sdeployment/web/theme.css\n' "$MP" "$MP" >&2
printf ' Searched: deployment/web/theme.compiled.css, deployment/web/theme.css under %s\n' "$MODEL_DIR" >&2
printf ' Run a build first. "Did the design system reach the app" cannot be answered from\n' >&2
printf ' source, and answering it from source is how this defect shipped.\n' >&2
printf ' This is NOT a pass.\n' >&2
Expand All @@ -124,9 +154,11 @@ fi

VIOLATIONS=0
WARNINGS=0
FAULTS=0

report() { printf 'FAIL %s\n %s\n' "$1" "$2"; VIOLATIONS=$((VIOLATIONS + 1)); }
warn() { printf 'WARN %s\n %s\n' "$1" "$2"; WARNINGS=$((WARNINGS + 1)); }
fault() { printf 'FAULT %s\n %s\n' "$1" "$2"; FAULTS=$((FAULTS + 1)); }

# ── Pass 0: read the design system ──────────────────────────────────────────────────────────
# Tokens: custom properties declared in a :root block. Values too, because a knob may be bound
Expand Down Expand Up @@ -261,19 +293,19 @@ done
# Only dead where the theme actually runs on custom properties. Where it does not, assigning a
# framework SCSS variable is the CORRECT bridge and must not be reported.
CSS_VARS_MODE="unknown"
if grep -rqE '^[[:space:]]*\$use-css-variables[[:space:]]*:[[:space:]]*true' "${MP}theme/web/" 2>/dev/null; then
if grep -rqE '^[[:space:]]*\$use-css-variables[[:space:]]*:[[:space:]]*true' "$THEME_WEB/" 2>/dev/null; then
CSS_VARS_MODE="true"
elif grep -rqE '^[[:space:]]*\$use-css-variables[[:space:]]*:[[:space:]]*false' "${MP}theme/web/" 2>/dev/null; then
elif grep -rqE '^[[:space:]]*\$use-css-variables[[:space:]]*:[[:space:]]*false' "$THEME_WEB/" 2>/dev/null; then
CSS_VARS_MODE="false"
fi
if [ "$CSS_VARS_MODE" = "true" ]; then
for f in "${MP}"theme/web/*.scss "${MP}"themesource/*/web/*.scss; do
for f in "$THEME_WEB"/*.scss "$MODEL_DIR"/themesource/*/web/*.scss; do
[ -f "$f" ] || continue
case "${f#"$MP"}" in themesource/atlas_*|themesource/atlas_*/*) continue ;; esac
case "$f" in */themesource/atlas_*) continue ;; esac
while IFS= read -r hit; do
[ -z "$hit" ] && continue
ln="${hit%%:*}"
report "$f:$ln assigns a framework SCSS variable while the theme runs \$use-css-variables: true" \
report "$(rel "$f"):$ln assigns a framework SCSS variable while the theme runs \$use-css-variables: true" \
'That assignment compiles to nothing. Set the matching CSS custom property in the framework customization file instead.'
done <<EOF
$(grep -nE '^[[:space:]]*\$[a-z0-9-]+[[:space:]]*:[[:space:]]*var\(--' "$f" 2>/dev/null)
Expand All @@ -285,8 +317,10 @@ fi
MODEL_CLASS_TOTAL=0
MODEL_CLASS_MISSING=0
MODEL_MISSING_SAMPLE=""
if [ -d mdlsource ]; then
MODEL_CLASSES="$(grep -rhoE "Class:[[:space:]]*'[^']+'" mdlsource 2>/dev/null \
MDLSOURCE=""
for c in "$ROOT/mdlsource" "$MODEL_DIR/mdlsource"; do [ -d "$c" ] && { MDLSOURCE="$c"; break; }; done
if [ -n "$MDLSOURCE" ]; then
MODEL_CLASSES="$(grep -rhoE "Class:[[:space:]]*'[^']+'" "$MDLSOURCE" 2>/dev/null \
| sed "s/.*'\\(.*\\)'/\\1/" | tr ' ' '\n' | grep -E '^[A-Za-z_-][A-Za-z0-9_-]*$' | sort -u)"
for cls in $MODEL_CLASSES; do
MODEL_CLASS_TOTAL=$((MODEL_CLASS_TOTAL + 1))
Expand All @@ -300,9 +334,18 @@ if [ -d mdlsource ]; then
fi

# ── Verdicts ────────────────────────────────────────────────────────────────────────────────
if [ "$KNOB_TOTAL" -eq 0 ]; then
warn "no framework knobs found in $CUSTOM_VARS" \
'Pass 1 could not run. Set CUSTOM_VARS to the framework customization file, or say in the review why this project has none.'
# A knob file that is missing, or that declares nothing, means Pass 1 read nothing. "0 of 0
# knobs bound" under a clean verdict is the false green this used to print on every two-tree
# checkout, so it is a FAULT: exit 2, the pass is UNMEASURED, never clean.
KNOBS_LINE="$KNOB_BOUND of $KNOB_TOTAL framework knobs point at a design-system token"
if [ ! -f "$CUSTOM_VARS" ]; then
fault "framework customization file not found: $CUSTOM_VARS" \
"Pass 1 (knobs bound) did not run. Model directory: $MODEL_DIR. Pass the file as the third argument or CUSTOM_VARS=<path>; on a two-tree checkout check that the .mpr is under app/."
KNOBS_LINE="UNMEASURED — $CUSTOM_VARS not found"
elif [ "$KNOB_TOTAL" -eq 0 ]; then
fault "no framework knobs declared in $CUSTOM_VARS" \
'Pass 1 (knobs bound) read nothing: no uncommented --name: declarations. Point CUSTOM_VARS at the real framework customization file, or say in the review why this project has none.'
KNOBS_LINE="UNMEASURED — no knobs declared in $CUSTOM_VARS"
elif [ "$KNOB_BOUND" -eq 0 ]; then
report "0 of $KNOB_TOTAL framework knobs are bound to a design-system token" \
"The framework is still wearing its own defaults. Unbound:${UNBOUND_SAMPLE:- (all)}"
Expand Down Expand Up @@ -334,7 +377,8 @@ fi
printf '\n'
printf 'design system %s\n' "$DESIGN"
printf 'built stylesheet %s\n' "$BUILT"
printf 'knobs bound %s of %s framework knobs point at a design-system token\n' "$KNOB_BOUND" "$KNOB_TOTAL"
printf 'knob file %s\n' "$CUSTOM_VARS"
printf 'knobs bound %s\n' "$KNOBS_LINE"
printf 'tokens arrived %s of %s\n' "$TOKENS_ARRIVED" "$TOKEN_COUNT"
printf 'tokens valued %s of %s carry the design value in a bare :root\n' "$TOKENS_VALUED" "$TOKEN_COUNT"
printf 'classes arrived %s of %s\n' "$CLASSES_ARRIVED" "$CLASS_COUNT"
Expand All @@ -347,6 +391,10 @@ if [ "$VIOLATIONS" -gt 0 ]; then
printf '—-the tokens were never the problem, which is why every other check stayed green.\n'
exit 1
fi
if [ "$FAULTS" -gt 0 ]; then
printf '\ncheck-design-reaches-app: %s pass(es) could not run — UNMEASURED, NOT a pass.\n' "$FAULTS"
exit 2
fi
if [ "$WARNINGS" -gt 0 ]; then
printf '\ncheck-design-reaches-app: clean, %s warning(s) — read them.\n' "$WARNINGS"
exit 0
Expand Down
29 changes: 29 additions & 0 deletions tests/wave2/fixtures/design-reaches-app/CAPTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,32 @@ no selector altered, no structure simplified.

The 0-of-35 and 0-of-28 rows are the whole reason this instrument exists: both were true
while `mx check`, `mxcli lint`, the MDL suite and two e2e journeys were green.

## The two-tree layout (case 9), captured 2026-09-28

Case 9 builds its project in the shape of a real two-tree checkout, a Mendix 11.14.0 app
(mxbuild 11.14.0, Atlas 3, `$use-css-variables: true`) with the model under `app/`. The
paths, verbatim from that checkout:

| Path (relative to the repo root) | Holds |
|---|---|
| `design/ds.css` | the design system: tokens and classes |
| `mdlsource/` | the MDL scripts the WARN pass reads |
| `app/<Name>.mpr` + `app/mprcontents/` | the model |
| `app/theme/web/custom-variables.scss` | the framework knobs (664 lines, 35 uncommented declarations) |
| `app/themesource/<module>/web/` | theme modules |
| `app/deployment/web/theme.compiled.css` | the built sheet (1.2 MB) |

Measured there on 2026-09-28, with the built sheet passed by hand and nothing else:

| | Before the fix (knob file read from the root) | After the fix (no arguments at all) |
|---|---|---|
| knobs bound | **0 of 0** | **26 of 30** |
| tokens arrived | 157 of 157 | 157 of 157 |
| classes arrived | 239 of 269 | 239 of 269 |
| `$use-css-variables` | unknown (dead-bridge pass silently skipped) | true |
| verdict | `clean, 3 warning(s)`, exit 0 | `clean, 2 warning(s)`, exit 0 |

The 35 declarations include some that name no knob the built bare `:root` defines; those
drop out of the denominator. The "before" verdict is the defect: a pass that read nothing,
reported as clean.
Loading
Loading