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 @@ -22,6 +22,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are.
- 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
- fix(project-bin/exec.sh): **the delta gate keys an error on code + message + location, counted, instead of the message text alone.** On a real 22-error baseline (Mendix 11.12.1), 20 errors shared two "Could not find widget" messages and CE0117 always reads "Error(s) in expression.", so a script adding another CE0117 in a new microflow, or another copy of a baseline error, was judged "identical" and kept. Now each Error is a hash of errorCode, message and every location's module / document / element, and the subset test is a multiset test. Names, not GUIDs, so a CREATE OR REPLACE that leaves a pre-existing error in place still reads as pre-existing. Field-run on a scratch copy of a greenfield PoC model — MendixMau
- learn(workflow-structure-rules, learned-workflow-patterns, module-folder-convention, learned-mdl-preflight, bug-logs): **five workflow rules re-probed on mxcli v0.24.0 / Mendix 11.13.0.** A forward `JUMP TO` builds clean (the CE6681 row taught direction as a platform rule; it was the jump-named-after-its-target defect, fixed upstream in v0.21.0); an interrupting boundary timer ending in `jump to` or `end workflow` builds clean (BUG-109 stamped no-longer-reproduces); BUG-76 splits — the bare-enum `DECISION` is now refused by `MDL-WF03` and still corrupts when forced, a Boolean `true`/`false` decision builds at 0 errors; `create or modify workflow … folder` places the workflow. New: one interrupting boundary per activity (`MDL-WF15`/CE6697) and no `end workflow` on a non-interrupting path (`MDL-WF08`/CE1844); `SET TASK OUTCOME` needs a signed-in named user ("Only named users can complete user task"), with the run-as-session Java action pattern. Evidence: a six-probe, four-control construct run (check, exec, native `mx check`, describe read-back) and a live proof test — card-disbursement requirements-driven build
- fix(project-tests/e2e/otel.js): **`capture()` pages back to t0 instead of reading only the newest 400 traces.** Jaeger answers the newest `limit` traces and says nothing about the rest; with timers running, 400 traces were two minutes, so a capture with t0 15 minutes back reported 0 errors over 24 real ERROR spans. It now pages by `end` while a page is full and still after t0, dedupes by trace, caps at `OTEL_MAX_PAGES` (25) and marks the result `.truncated` when the cap stops it short. Field run against the live Jaeger: t0 −15 min, 462 spans / 0 errors → 15,439 spans / 24 errors in 11 pages; a step's own capture stays one page — card-disbursement requirements-driven build
- fix(project-bin/constants-audit.sh): **a `__SET_ME__`-style sentinel default reports `SENTINEL`, not `MODEL-SECRET`.** The audit only knew SET vs EMPTY, so the placeholder `learned-constants-and-secrets.md` Step 3 prescribes for a must-override secret got the same verdict as a real password in git and needed a waiver. The test reads the value's shape (`__[A-Za-z0-9_]+__`) and still prints nothing; the skill's verdict table gains the row. Field run on a copy of the model: 4 findings → 3, the hub password constant SENTINEL — card-disbursement requirements-driven build
Expand Down
63 changes: 40 additions & 23 deletions project-bin/exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
#
# The mxbuild gate is a DELTA gate, not an absolute "0 errors" one: a write is kept
# when the post-exec error set is the pre-flight baseline exactly, or a strict
# SUBSET of it (every post-exec error message already existed before this script
# ran — nothing new, even if some were cleared). Only a set containing something
# new triggers the snapshot restore. See "Delta gate" below.
# Known limit: errors are compared by message text only, so a new error whose
# message matches one already in the baseline is not seen as new.
# SUBSET of it (every post-exec error already existed before this script ran —
# nothing new, even if some were cleared). Only a set containing something new
# triggers the snapshot restore. See "Delta gate" below. An error is identified by
# code + message + where (module / document / element), counted, not deduplicated.
#
# Usage: ./bin/exec.sh <script.mdl>
# ./bin/exec.sh --patch <script> [args...]
Expand Down Expand Up @@ -554,20 +553,40 @@ err_codes() {
[ -n "$PY" ] || { echo "?"; return; }
"$PY" -c "import json;d=json.load(open('$(native_path "$1")'));print(','.join(sorted({x.get('errorCode','?') for x in d.get('problems',[]) if x.get('severity')=='Error'})))" 2>/dev/null || echo "?"
}
# err_set <errors.json> — one key per Error problem: a short hash of errorCode + message +
# every location's module / document / element, sorted, duplicates KEPT, joined with "|".
# The key used to be the message text alone. On a real 22-error model (Mendix 11.12.1),
# 20 of the 22 shared two messages ("Could not find widget 'Markdown viewer' …" x15,
# "… 'Events' …" x5), and mxbuild's CE0117 message is always "Error(s) in expression." —
# so while one CE0117 or one missing widget was in the baseline, a script could add any
# number more and the gate kept it.
# Names, not elementId GUIDs: a CREATE OR REPLACE re-mints the GUIDs of a microflow whose
# pre-existing error it leaves in place, and that must still read as pre-existing. A rename
# does read as new, and restores — the safe direction. Hex tokens: no "|" or glob characters,
# whatever the message says.
err_set() {
[ -n "$PY" ] || { echo ""; return; }
"$PY" -c "import json;d=json.load(open('$(native_path "$1")'));print('|'.join(sorted(x.get('message','') for x in d.get('problems',[]) if x.get('severity')=='Error')))" 2>/dev/null || echo ""
"$PY" - "$(native_path "$1")" <<'ERRSETPY' 2>/dev/null || echo ""
import hashlib, json, sys
d = json.load(open(sys.argv[1]))
keys = []
for x in d.get('problems', []):
if x.get('severity') != 'Error':
continue
locs = sorted('\x1e'.join(str(l.get(k) or '') for k in ('module', 'document', 'element'))
for l in (x.get('locations') or []))
raw = '\x1f'.join([x.get('errorCode') or '', x.get('message') or ''] + locs)
keys.append(hashlib.sha1(raw.encode('utf-8')).hexdigest()[:16])
print('|'.join(sorted(keys)))
ERRSETPY
}

# is_subset_of <candidate> <superset> — both are err_set's own "|"-joined sorted
# message strings (the SAME comparison key the identical-baseline check already
# used; this does not introduce a new one). True (exit 0) when every message in
# <candidate> also appears in <superset> — i.e. nothing NEW. An empty <candidate>
# (0 post-exec errors) is trivially a subset. Bash-native, no extra Python call:
# both strings are already in hand as plain variables by the time this runs.
# Pure POSIX word-splitting on IFS='|', no arrays/`read -a` — bash 3.2 / Git Bash
# safe. Messages containing a literal "|" would break the membership test the
# same way they already break the exact-equality test above; not new exposure.
# is_subset_of <candidate> <superset> — both are err_set's "|"-joined sorted key lists.
# True (exit 0) when <candidate> is a sub-MULTISET of <superset>: each key in <candidate>
# consumes one occurrence in <superset>, so a baseline holding an error once does not
# excuse the same error twice. An empty <candidate> (0 post-exec errors) is trivially a
# subset. Bash-native, no extra Python call. Word-splitting on IFS='|' and ${var/pat/rep},
# no arrays or `read -a` — bash 3.2 / Git Bash safe.
is_subset_of() {
_cand="$1"
[ -z "$_cand" ] && return 0
Expand All @@ -578,7 +597,7 @@ is_subset_of() {
IFS="$_oldIFS"
[ -z "$_e" ] && continue
case "$_super" in
*"|$_e|"*) : ;;
*"|$_e|"*) _super="${_super/|$_e|/|}" ;;
*) return 1 ;;
esac
IFS='|'
Expand Down Expand Up @@ -765,17 +784,15 @@ if [ -x "$MXBUILD" ] && [ -x "$JAVA_EXE" ]; then
# A shared model means another workstream can leave it non-building; an
# absolute "zero errors" gate would then block every good script forever.
# Keeps the write when the POST-exec error set is the baseline exactly
# (nothing changed), OR a STRICT SUBSET of it (every post-exec message
# already existed pre-exec — no new message, even though some
# pre-existing ones may have been cleared). Messages only: a new error
# with the same text as a baseline one passes (known limit, see header).
# (nothing changed), OR a STRICT SUBSET of it (every post-exec error
# already existed pre-exec — nothing new, even though some pre-existing
# ones may have been cleared). An error is code + message + location,
# counted (see err_set), so a second copy of a baseline error is new.
# Only a set with something NEW in it restores the snapshot.
# Real incident: a script that took 34
# pre-existing errors down to 1 was rolled back and logged
# "blocked: PRE-EXISTING CE1613" because the gate could only recognise
# "unchanged," never "reduced." Comparison key is unchanged — err_set's
# per-message string (see err_set above) — subset-checked by
# is_subset_of, not switched to a different key.
# "unchanged," never "reduced."
POST_SET=$(err_set "$ERRORS_FILE")
DELTA_KIND=""
if [ -n "$BASELINE_SET" ] && [ "$BASELINE_SET" = "$POST_SET" ]; then
Expand Down
Loading