diff --git a/CHANGELOG.md b/CHANGELOG.md index dee96878..6b88648e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m `docs/RELEASE-PROCESS.md` §8. ## Unreleased +- fix(test-stack-up): **the local lane gets explicit database choices, probes its JDK before building (exit 4 only when no JDK is found at all; an older JDK than expected, an unreadable version or `21-ea` only warns and the start proceeds, so a Mendix 10 project on JDK 17 keeps working; no silent container), and snapshots a split model first** — `RUN_LOCAL_DB` in stack.conf: `auto` (default) never switches database by itself: no database option, no TCP probe, mxcli arguments identical to before (Postgres on another port or host keeps working through the model's settings); if the start fails with `database not reachable` the script names the two choices, `hsqldb` (built-in file database, data separate from Postgres; used only when set) and `ensure` (`--ensure-db`, creates the local role and database); `postgresql` requires the server to answer on `RUN_LOCAL_DB_HOST`. A non-default mode is warned and published as `LOCAL_DB=` in stack.env. The split-model snapshot adds `/.mpr-snapshots/` to the project's `.gitignore` the first time. Cold start measured on a copy of a real split-model project: master fails with `database not reachable`, `RUN_LOCAL_DB=hsqldb` serves HTTP 200 in 46 s, model still split — outside review of v2026.11.12 +- fix(placeholders, gate-check): **an agent-stub slot whose text wraps onto a second line is now counted** by the shared slot count (`bin/lib/placeholders.sh`: init, sync, status, `check-stubs-filled.sh`) and by the build-ready placeholder check, which now FAILs on it instead of warning (maintainer decision 2026-11-13, supersedes the #509 warning): a project whose agent stubs still have the multi-line `PROJECT_LOCAL_SOURCES_OF_TRUTH` slot unfilled goes from a warning to a FAIL at build-ready; a fresh set of stubs reads 37 unfilled slots, not 36 (mdl-agent's `PROJECT_LOCAL_SOURCES_OF_TRUTH`, four lines long); `sync-project.sh`'s "untouched stub" (overwrite) test is deliberately unchanged, left per-line on purpose, so a stub with only that slot unfilled is still never overwritten — outside review of v2026.11.12 +- new(exec): **`exec.sh` prints one `MXTK-RESULT exec verdict= applied= verified= lint_new= exit=` line on every path that reached a verdict, before the closing lines, so an orchestrator reads the outcome without knowing that exit 4 means "applied, lint only"**; a failed restore reads `failed` / `applied=unknown` on the partial path too; no exit code or existing line changes; new fixture `tests/wave2/test-exec-result-line.sh` — outside review of v2026.11.12 +- fix(design-reaches-app): **the CSS readers now normalise both stylesheets first and match whole classes (#670)**: a one-line `:root{--a:1;--b:2}` read as 1 token with `}` in its value, a minified built sheet as having no bare `:root`, a rule inside `@media` or a multi-line selector list was missed, and `var(--x, fallback)` did not count as using `--x`. All five fixed by one `normalise_css()` step (comments dropped, one selector/declaration/`}` per line, `;` inside `url(...)`/strings kept). On the committed real pair (design and built sheets of a fictional test app, 2026-11) tokens arrived went from 9 of 10 to 32 of 38 and theme knobs bound from 10 of 46 to 31 of 46; the check takes about 7 s instead of 3 s on that pair, because it now reads all the tokens. **Can turn a project red:** class presence was a substring match, so `.card` counted as arrived when only `.card-header` existed; it is now a whole-class match, and a project that passed on that false reading can now report `0 of N design-system classes` (FAIL) or fewer classes arrived. No verdict names or exit codes changed — #670; outside review of v2026.11.12 +- docs(wrap-up): **the closing action of `checkpoint-cutover.md` now names the gate-check "N toolkit/mxcli bug(s) logged but not filed" line and the command to run (`bin/file-bugs.sh --auto` or `--links`)**; the line was already printed on a full run and on `--closeout`, but nothing at wrap-up said what to do with it; `--file` stays human-only; no always-loaded words added — outside review of v2026.11.12 ## v2026.11.12 - new(parallel-builds): **`skills/parallel-builds.md`, an optional reference outside the pipeline (experimental tier, no stage map, no agent stub; used only when the user asks): several builders on one app in git worktrees, and how to merge their branches without losing a module (#741)**: a plain `git merge` of the model left one branch's units unregistered and both build gates reported 0 errors; the skill sets up worktrees (ports, database, number blocks, owners), forbids the plain merge, names the merge paths per CLI version, and checks `mxcli fix hashes`, `show modules` and the shared units before the build gate; forbids `diag --check-units --fix` after a suspect merge; a field run, 2026-11, lost a 63-unit module. Verbatim `fix hashes` capture under `tests/wave2/fixtures/worktree-merge/`. Guard script, exec.sh merge mode and lint delta are not built — field reports #741 and #744; LoanDesk worktree merge test diff --git a/bin/gate-check.sh b/bin/gate-check.sh index b939ad45..695080b2 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -2435,8 +2435,16 @@ check_build_ready() { # positive made deleting the safety net the cheapest way to get a green gate. # A check that punishes the correct state trains people to break it. local remaining - remaining=$(sed 's/{{DOUBLE_BRACE}}//g' "$agents_dir"/*.md 2>/dev/null \ - | grep -o '{{[^}]*}}' | sort -u | tr '\n' ' ') + # tr joins each file's lines first so a placeholder wrapped over several lines counts (#509); + # per file, and the body excludes `{`, so the `'{{'` quoted in prose on line 11 of every stub + # cannot run on to a later slot's `}}` (see bin/lib/placeholders.sh). + # A slot longer than 80 characters (a wrapped one carries its whole hint text) is printed as + # {{NAME…}} so the failure line stays readable; shorter slots print unchanged. + remaining=$(for _af in "$agents_dir"/*.md; do + [ -f "$_af" ] && sed 's/{{DOUBLE_BRACE}}//g' "$_af" 2>/dev/null | tr '\n' ' '; echo + done | grep -o '{{[^{}]*}}' | sort -u \ + | awk '{ if (length($0) > 80) { n=$0; sub(/^\{\{/, "", n); sub(/[^A-Za-z0-9_].*$/, "", n); print "{{" n "…}}" } else print }' \ + | tr '\n' ' ') if [ -n "$missing_agents" ]; then echo " ✗ missing agent(s):$missing_agents — run bin/init-agents.sh" fails=$((fails+1)) @@ -2449,18 +2457,17 @@ check_build_ready() { echo " ✗ agent(s) still have unfilled placeholders: ${remaining}— complete them (agent-roles.md)" fails=$((fails+1)) else - # A placeholder whose description spans lines (`{{NAME — prose…` on one line, `…}}` on a - # later one — mdl-agent's PROJECT_LOCAL_SOURCES_OF_TRUTH is four lines long) never matched - # the per-line grep above, so a stub with it untouched read "no unfilled placeholders" - # (#509). WARN, not FAIL: every project scaffolded before this check would flip to FAIL on - # a line nobody was ever told about; whether it should block is a maintainer call. + # Decision (maintainer, 2026-11-13, supersedes the #509 warning): an unfilled multi-line + # placeholder FAILs build-ready, like any other unfilled slot. The joined-lines count above + # already catches it; this perl scan is kept only for the one shape that count cannot match, + # a slot whose own text contains a single `{` (the count's body is `[^{}]*`). local multi="" multi=$(sed 's/{{DOUBLE_BRACE}}//g' "$agents_dir"/*.md 2>/dev/null \ | perl -0777 -ne 'while (/\{\{([A-Z][A-Z0-9_]+)[^}\n]*\n.*?\}\}/sg) { print "{{$1…}}\n" }' 2>/dev/null \ | sort -u | tr '\n' ' ') if [ -n "$multi" ]; then - echo " ✓ all 6 agents present, no single-line placeholders" - echo " ⚠ multi-line placeholder(s) still unfilled: ${multi}— complete them (agent-roles.md); WARN only, not counted as a FAIL" + echo " ✗ agent(s) still have unfilled multi-line placeholders: ${multi}— complete them (agent-roles.md)" + fails=$((fails+1)) else echo " ✓ all 6 agents present, no unfilled placeholders" fi diff --git a/bin/lib/placeholders.sh b/bin/lib/placeholders.sh index 9302be6a..2135404e 100644 --- a/bin/lib/placeholders.sh +++ b/bin/lib/placeholders.sh @@ -20,7 +20,12 @@ # Bash 3.2 + POSIX sed/grep. Never fails: an unreadable file is skipped. _mxtk_slots() { # $1=file → one slot name per line - sed 's/{{DOUBLE_BRACE}}//g' "$1" 2>/dev/null | grep -o '{{[^}]*}}' \ + # tr joins the lines first: a slot whose text wraps onto a second line is still ONE slot + # (mdl-agent's PROJECT_LOCAL_SOURCES_OF_TRUTH is four lines long; the per-line grep missed it). + # The body excludes `{` too: line 11 of every agent stub quotes a bare `'{{'` in prose, and + # once lines are joined that `{{` would otherwise run on to the next real slot's `}}`, + # swallowing every slot in between. + sed 's/{{DOUBLE_BRACE}}//g' "$1" 2>/dev/null | tr '\n' ' ' | grep -o '{{[^{}]*}}' \ | sed -e 's/^{{ *//' -e 's/[^A-Za-z0-9_].*$//' -e 's/}}$//' } diff --git a/project-bin/check-design-reaches-app.sh b/project-bin/check-design-reaches-app.sh index 273eaa06..ad3fbae3 100755 --- a/project-bin/check-design-reaches-app.sh +++ b/project-bin/check-design-reaches-app.sh @@ -171,6 +171,56 @@ 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)); } +# ── Normalise both stylesheets once (#670) ───────────────────────────────────────────────── +# Every reader below is line-based and was written for a pretty-printed sheet: one declaration +# per line, `:root {` alone on its line. A minified or one-line sheet (`:root{--a:1;--b:2}`) +# read as ONE token whose value kept its `}`, and a minified built sheet as having no bare +# :root at all — the report then printed denominators like "1 of 1" over almost nothing. +# So: drop /* comments */, then put every selector (`sel {`), declaration (`prop: value;`) and +# closing `}` on a line of its own. `;` `{` `}` inside (...) or "..." / '...' (url(data:...;base64,..)) do not +# split; a multi-line selector list collapses onto one line; a rule inside @media gets its own +# line. A sheet that was already one-declaration-per-line comes out with the same lines. +normalise_css() { + awk ' + function emit(s) { gsub(/[ \t\r]+/, " ", s); sub(/^ /, "", s); sub(/ $/, "", s); return s } + BEGIN { SPECIAL = "[/*\"(){};\\\\\047]"; buf = ""; depth = 0; q = ""; inc = 0 } + { + line = $0 + if (!inc && q == "" && depth == 0 && line !~ SPECIAL) { buf = buf " " line; next } + n = length(line) + for (i = 1; i <= n; i++) { + c = substr(line, i, 1) + if (inc) { if (c == "*" && substr(line, i + 1, 1) == "/") { inc = 0; i++; buf = buf " " } continue } + if (q != "") { + buf = buf c + if (c == "\\") { buf = buf substr(line, i + 1, 1); i++ } else if (c == q) q = "" + continue + } + if (c == "/" && substr(line, i + 1, 1) == "*") { inc = 1; i++; continue } + if (c == "\"" || c == "\047") { q = c; buf = buf c; continue } + if (c == "\\") { buf = buf c substr(line, i + 1, 1); i++; continue } + if (c == "(") { depth++; buf = buf c; continue } + if (c == ")") { if (depth > 0) depth--; buf = buf c; continue } + if (depth == 0 && c == "{") { s = emit(buf); print (s == "" ? "{" : s " {"); buf = ""; continue } + if (depth == 0 && c == ";") { s = emit(buf); if (s != "") print s ";"; buf = ""; continue } + if (depth == 0 && c == "}") { s = emit(buf); if (s != "") print s; print "}"; buf = ""; continue } + buf = buf c + } + q = ""; buf = buf " " + } + END { s = emit(buf); if (s != "") print s } + ' "$1" +} +NORM_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cdra.XXXXXX")" +trap 'rm -rf "$NORM_DIR"' EXIT +DESIGN_N="$NORM_DIR/design.css"; BUILT_N="$NORM_DIR/built.css" +normalise_css "$DESIGN" > "$DESIGN_N" +normalise_css "$BUILT" > "$BUILT_N" + +# A whole-class match: `.card` is present when `.card` is followed by a character that cannot +# continue a class name, never when only `.card-header` exists (a substring match counted it). +has_class() { grep -qE -- "$(printf '%s' "$2" | sed 's/\./\\./g')([^A-Za-z0-9_-]|\$)" "$1"; } + # ── Pass 0: read the design system ────────────────────────────────────────────────────────── # Tokens: custom properties declared in a :root block. Values too, because a knob may be bound # by repeating the literal rather than by var() — still bound, and still correct. @@ -179,15 +229,15 @@ TOKENS="$(awk ' inroot && /^[[:space:]]*}/ { inroot = 0 } inroot && match($0, /--[a-z0-9-]+[[:space:]]*:/) { name = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", name) - rest = substr($0, RSTART + RLENGTH); sub(/;.*$/, "", rest) + rest = substr($0, RSTART + RLENGTH); sub(/;[[:space:]]*$/, "", rest) gsub(/^[[:space:]]+|[[:space:]]+$/, "", rest) if (!(name in seen)) { seen[name] = 1; print name "\t" rest } } -' "$DESIGN")" +' "$DESIGN_N")" TOKEN_COUNT="$(printf '%s\n' "$TOKENS" | grep -c '^--' || true)" # Classes: any `.name` appearing at the head of a rule. Compound selectors contribute each. -CLASSES="$(grep -oE '^[^{@}]*\{' "$DESIGN" \ +CLASSES="$(grep -oE '^[^{@}]*\{' "$DESIGN_N" \ | grep -oE '\.[A-Za-z_-][A-Za-z0-9_-]*' | sort -u)" CLASS_COUNT="$(printf '%s\n' "$CLASSES" | grep -c '^\.' || true)" @@ -211,7 +261,7 @@ BUILT_ROOT="$(awk ' map[name] = rest } END { for (k in map) print k "\t" map[k] } -' "$BUILT")" +' "$BUILT_N")" # Themed palettes. A design system may keep its whole palette in scoped blocks, # `:root.theme-light { ... }`, `:root.theme-dark { ... }`, with no bare `:root` at all. Pass 1 @@ -227,21 +277,21 @@ scoped_root() { inscope && /^[[:space:]]*\}/ { inscope = 0; next } inscope && match($0, /--[a-zA-Z0-9-]+[[:space:]]*:/) { name = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", name) - rest = substr($0, RSTART + RLENGTH); sub(/;.*$/, "", rest) + rest = substr($0, RSTART + RLENGTH); sub(/;[[:space:]]*$/, "", rest) gsub(/^[[:space:]]+|[[:space:]]+$/, "", rest) key = sel "\t" name; if (!(key in map)) order[++n] = key; map[key] = rest } END { for (i = 1; i <= n; i++) print order[i] "\t" map[order[i]] } ' "$1" } -DESIGN_SCOPED="$(scoped_root "$DESIGN")" -BUILT_SCOPED="$(scoped_root "$BUILT")" +DESIGN_SCOPED="$(scoped_root "$DESIGN_N")" +BUILT_SCOPED="$(scoped_root "$BUILT_N")" # Names the design declares in a bare `:root`; every other token is themed-only. DESIGN_BARE_NAMES="$(awk ' /^[[:space:]]*:root[[:space:]]*\{[[:space:]]*$/ { inroot = 1; next } inroot && /^[[:space:]]*\}/ { inroot = 0; next } inroot && match($0, /--[a-zA-Z0-9-]+[[:space:]]*:/) { n = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", n); print n } -' "$DESIGN" | sort -u)" +' "$DESIGN_N" | sort -u)" KNOB_TOTAL=0 KNOB_BOUND=0 @@ -263,10 +313,12 @@ if [ -f "$CUSTOM_VARS" ]; then val="$(printf '%s\n' "$BUILT_ROOT" | awk -F'\t' -v k="$knob" '$1 == k { v = $2 } END { print v }')" [ -z "$val" ] && continue bound=0 + vals="$(printf '%s' "$val" | tr -d ' ')" # (a) bound by reference while IFS="$(printf '\t')" read -r tname tval; do [ -z "$tname" ] && continue - case "$val" in *"var($tname)"*) bound=1; break ;; esac + # `var(--x)` and `var(--x, fallback)` both use --x + case "$vals" in *"var($tname)"*|*"var($tname,"*) bound=1; break ;; esac # (b) bound by repeating the literal. Only for values that look like a value, so an # empty or inherit-ish token cannot mark every knob bound. if [ -n "$tval" ] && [ "${#tval}" -ge 3 ]; then @@ -289,7 +341,7 @@ fi TOKENS_ARRIVED=0 while IFS="$(printf '\t')" read -r tname tval; do [ -z "$tname" ] && continue - grep -qE -- "$tname[[:space:]]*:" "$BUILT" && TOKENS_ARRIVED=$((TOKENS_ARRIVED + 1)) + grep -qE -- "$tname[[:space:]]*:" "$BUILT_N" && TOKENS_ARRIVED=$((TOKENS_ARRIVED + 1)) done < applied= verified= lint_new= exit= +# Printed once on every path that reached a verdict, BEFORE that path's closing human lines, so it +# is never the last line of output (the last line stays the one documented above). Built by +# result_line() only. verdict words: +# applied exit 0 kept; gate passed (verified=yes) +# applied-lint exit 4 (1 under `Lint gate: hard`) kept; gate passed; lint rose or blind +# applied-unverified exit 5 kept; the gate could not check the model (verified=no) +# partial exit 1 mxcli exec failed part-way and the statements before it are live (applied=partial) +# restored exit 1 NOT kept: a failed gate, a failed or refused --patch, rolled back (applied=no) +# failed exit 1 NOT kept as intended but the rollback did not run (applied=unknown): restore by hand +# refused exit 1 a guard stopped it before anything was written (applied=no, verified=skipped) +# verified: yes = the gate passed; no = it ran or could not run and did not pass; skipped = never reached. +# lint_new: = findings new this script when lint measured them (0 = clean). Otherwise one word: +# unknown lint rose vs baseline but the rise could not be itemised (no Python / the diff failed) +# blind lint could not run or measure (a blind rule, reverted rules, lint-gate fault, no baseline) +# skipped lint was skipped (SKIP_LINT) or bin/lint-gate.sh is not installed +# na lint never ran on this path (a refusal, a restored write, a --patch) +# NO LINE: exit 2 (usage error), a missing .mpr, and any abort under `set -e` before a verdict +# (for example a failing snapshot step) print no MXTK-RESULT line. Read "no MXTK-RESULT line" as +# "no verdict reached", never as success. The line goes to STDOUT; some refusal text goes to +# STDERR, so capture both streams (2>&1) when reading a run. +# Restore failed (partial or gate path): verdict=failed applied=unknown on BOTH paths. # Until 2026-10-20, 4 was 1 and 5 was 0: in one field build 57 of 58 clean builds exited 1 on a # lint rise, agents learned to ignore 1, and 10 UNVERIFIED runs exiting 0 read as success. # A PARTIAL run, or a --patch script that failed, used to pass the script's own exit code @@ -150,6 +173,18 @@ fi # A failed script's own exit code becomes 1: passed through, its 2 would read as "nothing # touched" and its 4 or 5 as "applied" (see Exit codes above). fail_rc() { case "$1" in 0) echo 0 ;; *) echo 1 ;; esac; } +# MXTK-RESULT (see the header): the one place that builds the line. result_line +# . Printed BEFORE a path's closing human lines, never as the last line of output. +LINT_NEW=na +result_line() { echo "MXTK-RESULT exec verdict=$1 applied=$2 verified=$3 lint_new=${LINT_NEW:-na} exit=$4"; } +# The closing human line of the paths whose verdict needs RESTORED. Three cases, because two of +# these paths never attempt a restore (a partial run; mxbuild crashed with no baseline, where the +# write is kept on purpose): telling the reader to "restore" there would contradict the text above. +restore_close() { + if [ "${RESTORED:-0}" -eq 1 ]; then echo " Rolled back: the model is as it was before this run." + elif [ "${RESTORE_TRIED:-0}" -eq 1 ]; then echo " NOT rolled back: restore the model before the next exec (see above)." + else echo " No rollback was attempted: the model holds what this run wrote (see above)."; fi +} # ── Change class (advisory, added 2026-10-13) ──────────────────────────────── # Tags each run `ui-only` or `logic`, so the run summary can say which TEST LANE applies @@ -733,6 +768,7 @@ if [ "$PATCH_MODE" != "1" ]; then echo "$_scan_out" case "${MXTK_SCAN_OVERRIDE:-}" in ""|1|true|yes) + result_line refused no skipped 1 echo "" echo "✗ STOP-row scan: refusing exec on a blocking rule. NOTHING was locked, snapshotted or written." echo " Each line above names its learned-mdl-preflight.md row and the rewrite. Fix the script." @@ -767,6 +803,7 @@ if [ "$PATCH_MODE" != "1" ] && [ -x "$PROJECT_ROOT/bin/row-groups.sh" ]; then if [ "$_rg_rc" = 1 ]; then echo "$_rg_out" if [ -z "${MXTK_ROWS_OVERRIDE:-}" ]; then + result_line refused no skipped 1 echo "✗ Row-group gate: refusing exec. NOTHING was locked, snapshotted or written." echo " See the groups: bin/row-groups.sh. Override with a reason: MXTK_ROWS_OVERRIDE=\"\" ./bin/exec.sh $SCRIPT" log_build "🚫 blocked" "row-group gate: $(printf '%s' "$_rg_out" | head -1 | tr '|' '/' | cut -c1-200) — nothing written" @@ -806,6 +843,7 @@ if [ -f "$MPR.lock" ]; then SP_PID=$(grep -oE '"ProcessId":[0-9]+' "$MPR.lock" 2>/dev/null | grep -oE '[0-9]+' || true) if [ -n "$SP_PID" ] && kill -0 "$SP_PID" 2>/dev/null; then SP_OPEN=1 + [ "$FORCE" = "1" ] || result_line refused no skipped 1 echo "✗ Studio Pro has $NAME open (lock PID $SP_PID alive) — refusing exec (split-brain corruption risk)." echo " → Close the project in Studio Pro (or quit SP), then re-run." echo " Override (NOT recommended): FORCE_EXEC=1 ./bin/exec.sh $SCRIPT" @@ -878,6 +916,7 @@ for _try in 1 2 3 4 5 6; do _age="" case "$_hsince" in ''|*[!0-9]*) ;; *) _age=" for $(( ${EPOCHSECONDS:-$(date +%s)} - _hsince ))s" ;; esac [ "$_hrest" = "$_held" ] && _hscript="?" + result_line refused no skipped 1 echo "✗ Another session holds this project's exec lock (PID $_hpid, running$_age, script: $_hscript)." echo " → Wait for it to finish, then re-run. Nothing was written." echo " (A dead holder is detected and taken over automatically — no manual rm is ever needed.)" @@ -890,6 +929,7 @@ for _try in 1 2 3 4 5 6; do fi done if [ "$LOCK_TAKEN" != 1 ]; then + result_line refused no skipped 1 echo "✗ Could not take the exec lock $LOCK after 6 attempts — another session keeps re-taking it." echo " → Wait for the other exec to finish, then re-run. Nothing was written." exit 1 @@ -914,6 +954,7 @@ MODEL_PATHS="$MPR" $MODEL_DIR/mprcontents" commit_model_first() { if [ "$SP_OPEN" = 1 ]; then + result_line refused no skipped 1 echo "✗ --commit-first refused: Studio Pro holds $NAME (PID $SP_PID). Its in-memory model is not" echo " what is on disk, so a commit now would record a state nobody is looking at." echo " → Save and close the project in Studio Pro, then re-run. Nothing was committed or executed." @@ -931,6 +972,7 @@ commit_model_first() { [ "$_other" -gt 0 ] && echo " ($_other other staged path(s) stay staged and are NOT part of this commit.)" if ! git add -- $MODEL_PATHS; then IFS="$_IFS"; set +f + result_line refused no skipped 1 echo "✗ --commit-first: git add of the model paths failed — nothing was committed or executed." log_build "🚫 blocked" "--commit-first: git add failed" return 1 @@ -940,6 +982,7 @@ commit_model_first() { -- $MODEL_PATHS 2>&1)"; then IFS="$_IFS"; set +f printf '%s\n' "$_cout" | sed 's/^/ /' + result_line refused no skipped 1 echo "✗ --commit-first: git commit was refused — nothing was executed. The model paths are left staged." echo " If the model-stamp hook refused it: ./bin/verify-model.sh (one mxbuild, stamps a clean model)," echo " then re-run with --commit-first." @@ -956,6 +999,7 @@ if [ -n "$MPR_DIRTY" ] && [ "$COMMIT_FIRST" = 1 ]; then MPR_DIRTY=$(git status --porcelain "$MPR" "$MODEL_DIR/mprcontents" 2>/dev/null | grep -v "^$" || true) fi if [ -n "$MPR_DIRTY" ]; then + [ "$FORCE" = "1" ] || result_line refused no skipped 1 echo "✗ Uncommitted model changes — refusing exec to prevent snapshot regression." echo "" echo "$MPR_DIRTY" | sed 's/^/ /' @@ -1130,6 +1174,7 @@ if [ "$PATCH_MODE" != "1" ] && [ -f "$SCRIPT" ] && [ -n "$_MKT_LIB" ] \ FAIL) echo "Marketplace installs: FAIL — $_mkt_nt" if [ -z "${MXTK_MKT_OVERRIDE:-}" ]; then + result_line refused no skipped 1 echo "" echo "✗ Marketplace install gate: refusing this domain-model script. NOTHING was snapshotted or written." echo " A confirmed fit-gap Buy is not in the model. Install it first (brd-to-build-plan.md Step 0)," @@ -1226,10 +1271,11 @@ run_lint_gate() { } if [ -n "${SKIP_LINT:-}" ]; then LINT_DETAIL="lint SKIPPED (SKIP_LINT=$SKIP_LINT)" + LINT_NEW=skipped echo " ⚠ lint ratchet skipped: SKIP_LINT=$SKIP_LINT"; return fi if [ ! -x "$PROJECT_ROOT/bin/lint-gate.sh" ]; then - LINT_RC=2; LINT_DETAIL="lint not installed (bin/lint-gate.sh missing — sync-project.sh installs it)" + LINT_RC=2; LINT_NEW=skipped; LINT_DETAIL="lint not installed (bin/lint-gate.sh missing — sync-project.sh installs it)" echo " ⚠ $LINT_DETAIL"; return fi echo "→ Running lint ratchet (bin/lint-gate.sh, read-only)..." @@ -1266,6 +1312,10 @@ run_lint_gate() { # lint-gate's non-blocking SEC014 note (#312: rule installed, decision not made) is otherwise # visible only on a failing run, because the capture above is shown only then. Always echo it. [ -z "$_L_SEC014" ] || printf '%s' "$_L_SEC014" + case "$LINT_RC" in + 0) LINT_NEW=0 ;; # a rise sets the real count in its own branch below + 2|3) LINT_NEW=blind ;; # could not run / not measured; a blind rise (LINT_RC 1 -> 3) is set below + esac case "$LINT_RC" in 0) LINT_DETAIL="lint unchanged vs baseline"; echo " ✓ lint: no rule rose vs baseline" ;; 1) # Rule ids are matched as [A-Z][A-Z0-9-]*[0-9]+, not [A-Z]+[0-9]+: mxcli's built-in @@ -1275,7 +1325,7 @@ run_lint_gate() { if [ "$_L_BLIND_STOCK" = 1 ]; then # `mxcli init` re-seeded the toolkit rules (#264): lint-gate did not lint. Logging this # as a rise blamed the next script for CONV010 26 -> 169 on flows it never touched. - LINT_RC=3 + LINT_RC=3; LINT_NEW=blind LINT_DETAIL="lint BLIND: rules reverted to stock (${_reverted:-see output}) — run sync-project.sh; delta not attributable" echo " ✗ lint: toolkit rules reverted to stock (${_reverted:-see output}) — not linted, not a rise" sed -n '/^!! LINT RULES REVERTED TO STOCK/,/^BLIND/p' "$_lo" | head -16 | sed 's/^/ /' @@ -1283,7 +1333,7 @@ run_lint_gate() { # A rule the project edited moved since the baseline (#524): mxcli's session-start # refresh re-seeded stock over it, or someone changed it. Not linted, not a rise. _projrev="$_L_PROJREV" - LINT_RC=3 + LINT_RC=3; LINT_NEW=blind LINT_DETAIL="lint BLIND: project-edited rule(s) changed since the baseline (${_projrev:-see output}) — git checkout, or re-baseline; delta not attributable" echo " ✗ lint: project-edited lint rule(s) changed since the baseline (${_projrev:-see output}) — not linted, not a rise" sed -n '/^!! PROJECT-EDITED LINT RULE/,/^BLIND/p' "$_lo" | head -16 | sed 's/^/ /' @@ -1291,7 +1341,7 @@ run_lint_gate() { # lint-gate exits 1 for PASS-BLIND too. Logging that as "ROSE: see output" made a rule # that inspected nothing look like a rise with no rule named — 20 such rows in two # field apps whose toolkit CONV020 (now MXTK-UX003) shipped unconfigured (#229). It is an instrument fault. - LINT_RC=3 + LINT_RC=3; LINT_NEW=blind LINT_DETAIL="lint BLIND: ${_blind:-a rule} inspected nothing (instrument fault, not a rise)" echo " ✗ lint: nothing rose, but ${_blind:-a rule} could not read the model — a clean count means nothing" sed -n '/^!! A RULE COULD NOT READ/,/^$/p' "$_lo" | head -12 | sed 's/^/ /' @@ -1337,6 +1387,7 @@ PY _thisnew=$([ -n "$_thispyexe" ] && "$_thispyexe" -c "$_thispy" "$PROJECT_ROOT/.claude/loop") _first="${_thisnew%%$'\n'*}"; _thisn="${_first%% *}"; _rest="${_first#* }"; _thisc="${_rest%% *}" if [ -z "$_thisnew" ]; then + LINT_NEW=unknown _noadv=1 # undiffed rise: keep prev-exec as is, or the next exec reads these as old debt # No Python, or the diff failed: never read that as "0 new". Report the rise vs baseline. LINT_DETAIL="lint rose: ${_rules:-see output} (per-script diff unavailable)" @@ -1344,10 +1395,12 @@ PY sed -n '/^FAIL/,$p' "$_lo" | head -60 | sed 's/^/ /' elif [ "${_thisn:-0}" = 0 ]; then LINT_RC=0 + LINT_NEW=0 LINT_DETAIL="lint: 0 new this script (baseline debt: ${_rules:-see lint-last.json})" echo " ✓ lint: this script added no finding (older debt vs baseline unchanged)" else # "lint rose" stays the wording, as before this change (metrics and readers key on it). + LINT_NEW="$_thisn" LINT_DETAIL="lint rose: +${_thisn} new or unfixed (${_thisc:-0} carried from earlier execs): ${_rest#* }" echo " ✗ lint: ${_thisn} finding(s) new this script or still unfixed from an earlier one — read each one:" case "$_thisnew" in *$'\n'*) printf '%s\n' "${_thisnew#*$'\n'}" ;; esac @@ -1433,6 +1486,7 @@ elif [ "${SKIP_CHECK:-0}" != "1" ] && [ -n "$PMXCLI" ]; then "$PMXCLI" check "$SCRIPT" -p "$MPR" --references 2>&1 | tee "$_co"; _crc=${PIPESTATUS[0]} if [ "$_crc" -ne 0 ]; then _why="$(check_refusal_reason "$_co")"; rm -f "$_co" + result_line refused no skipped 1 echo "" echo " ✗ mxcli check failed — refusing to exec. NOTHING was written to the model." echo " Fix the script, or re-run with SKIP_CHECK=1 if you know why the parser is wrong." @@ -1534,6 +1588,7 @@ if [ ! -x "$MXBUILD" ] || [ ! -x "$JAVA_EXE" ]; then [ -x "$MXBUILD" ] || echo " mxbuild not found/executable: ${MXBUILD:-} (set MXBUILD_PATH=)" >&2 [ -x "$JAVA_EXE" ] || echo " java not found/executable: ${JAVA_EXE:-} (set JAVA_HOME=)" >&2 else + result_line refused no skipped 1 echo "" >&2 echo "✗ REFUSING to exec: the mxbuild gate cannot run on this machine, so nothing could verify the write." >&2 [ -x "$MXBUILD" ] || echo " mxbuild not found/executable: ${MXBUILD:-} (MXBUILD_PATH= overrides; bin/doctor.sh --install downloads it)" >&2 @@ -2041,6 +2096,8 @@ if [ "$PATCH_MODE" = "1" ]; then log_build "❌ patch failed" "script exit $EXEC_STATUS — NOT rolled back (restore did not run)" fi rm -f "$HV_BASE" + if [ "$RESTORED" -eq 1 ]; then result_line restored no skipped 1; else result_line failed unknown skipped 1; fi + restore_close exit "$(fail_rc "$EXEC_STATUS")" fi if [ "$HV_ON" = "1" ]; then @@ -2128,6 +2185,8 @@ EOF_HV else log_build "❌ patch refused" "$_hv_detail — NOT rolled back (restore did not run)" fi + if [ "$RESTORED" -eq 1 ]; then result_line restored no skipped 1; else result_line failed unknown skipped 1; fi + restore_close exit 1 fi rm -f "$HV_BASE" "$HV_POST" @@ -2462,6 +2521,8 @@ if [ "$PATCH_MODE" = "1" ] && [ "$GATE_STATE" != "pass" ] && [ "$RESTORE_TRIED" else log_build "❌ patch unverified" "gate $GATE_STATE — NOT rolled back (restore did not run)" fi + if [ "$RESTORED" -eq 1 ]; then result_line restored no no 1; else result_line failed unknown no 1; fi + restore_close exit 1 fi @@ -2532,6 +2593,11 @@ if [ "$EXEC_STATUS" -ne 0 ]; then if [ -x ./bin/model-stamp.sh ]; then ./bin/model-stamp.sh clear >/dev/null 2>&1 || true fi + _pv=no; [ "$GATE_STATE" != pass ] || _pv=yes + if [ "$RESTORED" -eq 1 ]; then result_line restored no "$_pv" "$(fail_rc "$EXEC_STATUS")" + elif [ "$RESTORE_TRIED" -eq 1 ]; then result_line failed unknown "$_pv" "$(fail_rc "$EXEC_STATUS")" # same as the gate-fail path + else result_line partial partial "$_pv" "$(fail_rc "$EXEC_STATUS")"; fi + restore_close exit "$(fail_rc "$EXEC_STATUS")" fi @@ -2565,6 +2631,8 @@ if [ "$GATE_STATE" = "pass" ] && [ "${_gk_mode:-}" = "check" ]; then fi if [ "$GATE_STATE" = "fail" ]; then + if [ "$RESTORED" -eq 1 ]; then result_line restored no no 1; else result_line failed unknown no 1; fi + restore_close exit 1 fi @@ -2727,12 +2795,14 @@ if [ "$GATE_STATE" != "pass" ]; then echo " Nothing has verified the model, and it carries no verification stamp: the commit" echo " hook will refuse it until ./bin/verify-model.sh has run the gate over it." log_build "⚠️ applied, UNVERIFIED" "gate $GATE_STATE${MXB_WHY:+ (mxbuild exit ${MXBUILD_EXIT:-?}: $MXB_WHY)} — model not verified" + result_line applied-unverified yes no 5 echo "applied, UNVERIFIED (exit 5): run ./bin/verify-model.sh before the next exec; do not re-apply this script" exit 5 fi # Lint only: the write is kept and the gate passed, so the code is 4, never 1 (see Exit codes). if [ "$LINT_RC" -eq 3 ]; then + result_line applied-lint yes yes 4 echo "" echo "✗ Script applied and mxbuild is clean, but a LINT RULE IS BLIND — this is not a pass." echo " $LINT_DETAIL" @@ -2759,14 +2829,17 @@ if [ "$LINT_RC" -eq 1 ]; then esac case "$_lgm" in hard|soft|default|"") ;; *) echo "note: 'Lint gate: $_lgm' not recognised (hard | default)" ;; esac if [ "$_lgm" = hard ]; then + result_line applied-lint yes yes 1 echo "gate passed, LINT ROSE under 'Lint gate: hard' (exit 1: write kept, do not re-apply)" exit 1 fi + result_line applied-lint yes yes 4 echo "gate passed, lint only (exit 4: LINT ROSE; write kept, do not re-apply)" exit 4 fi echo "" +result_line applied yes yes 0 echo "✓ Script applied to $MPR_BASE." echo "" _EL=$(( ${EPOCHSECONDS:-$(date +%s)} - _T0 )) diff --git a/project-bin/test-stack-up.sh b/project-bin/test-stack-up.sh index 94274220..cac67ab8 100755 --- a/project-bin/test-stack-up.sh +++ b/project-bin/test-stack-up.sh @@ -63,7 +63,35 @@ # # Exit codes: 0 = required stack is up · 1 = not up and could not fix · 2 = usage/env error · # 4 = instrument blocked: the port `mxcli run --local` needs is held by another process -# (named with its pid, never stopped) — record it as blocked, not as a test failure +# (named with its pid, never stopped), OR the local lane's JDK / selected Postgres is +# missing (named; no silent fall-back to a container) — record it as blocked, not as a +# test failure +# +# LOCAL LANE, COLD START (mxcli v0.28 `run --local`). Lane rules are unchanged: container when a runtime +# answers, else local; MXTK_APP_LANE=local / --local force local. What the lane now does before and +# while starting: +# 1. DATABASE — RUN_LOCAL_DB in stack.conf, one of +# auto (default) NEVER switches database by itself: no database option, no TCP probe, +# arguments byte-identical to before. A project on Postgres 5433 or a remote host works +# through the model's own settings. If the start then fails with "database not +# reachable", the script prints the two explicit choices below. +# postgresql no option; the Postgres must answer on RUN_LOCAL_DB_HOST (host:port, default +# 127.0.0.1:5432), else exit 4 naming it. RUN_LOCAL_DB_HOST is passed as --db-host. +# ensure add --ensure-db: mxcli creates the local Postgres role + database if missing +# (RUN_LOCAL_DB_HOST probed and passed as for postgresql, but never blocks). +# hsqldb --db-type hsqldb, the built-in file database (no server; data under +# /deployment/data/database/hsqldb/, SEPARATE from any Postgres data). Used +# ONLY when set explicitly. +# A non-default mode is warned about and published to stack.env as LOCAL_DB=. +# The line "database: ..." says which ran. +# 2. PROBE — a JDK must exist (find_java / JAVA_HOME): none found → exit 4 naming it, before any build is +# started. A JDK older than the toolkit expects for the model's Mendix version (21 up to 11.13, 25 from +# 11.14; _mxtk_java_want says 21 for Mendix 9/10 too) or an unreadable version only WARNS and the start +# proceeds — mxcli gives the real verdict. With RUN_LOCAL_DB=postgresql the server must answer, else +# exit 4. Never an automatic switch to the container lane: that is a decision. +# 3. SPLIT MODEL — a model stored as mprcontents/ is snapshotted first with snapshot-mpr.sh (the same +# rotating .mpr-snapshots/ exec.sh uses) and its unit count is compared after the start; a drop +# is reported with the restore commands. Not reproduced with v0.28 (see the lane block below). # # PORT HELD BEFORE RUN --LOCAL (#262, field run 2026-10-10). Studio Pro had started its own runtime on # the app/admin ports a run --local restart needed. mxcli failed with "Authentication failed" (the @@ -106,6 +134,8 @@ # # APP_PORTS="8080 8081" # fallback scan list; ownership beats it # RUN_LOCAL_SERVE_PORT=6644 # local lane's mxbuild port (default: first free from 6543) +# RUN_LOCAL_DB=auto # local lane database: auto (model's own settings) | postgresql | ensure | hsqldb (explicit only) +# RUN_LOCAL_DB_HOST=127.0.0.1:5432 # Postgres host:port for postgresql/ensure (probed; passed as --db-host); ignored by auto # JAEGER_PORT=16686 # OTel collector UI (optional) # MOCK_HEALTH_URL="http://localhost:3001/api/health" # set to enable the mock rung # MOCK_PORT=3001 @@ -444,6 +474,7 @@ publish_stack_env() { echo "APP_OWNERSHIP=${3:-unknown}" echo "APP_SOURCE=${4:-unknown}" [ -n "$MOCK_PORT" ] && echo "MOCK_PORT=$MOCK_PORT" + [ -n "${LOCAL_DB_MODE:-}" ] && echo "LOCAL_DB=$LOCAL_DB_MODE" [ "$2" -eq 0 ] && echo "JAEGER_PORT=$JAEGER_PORT" || echo "JAEGER_PORT=" } > "$STACK_ENV" } @@ -810,20 +841,82 @@ if [ $APP_FOUND -ne 0 ]; then if [ -z "$_LS" ]; then for _p in $(seq 6543 6599); do mxtk_port_holder "$_p" >/dev/null 2>&1 || { _LS=$_p; break; }; done fi + # --- what the local lane needs, probed BEFORE anything is built (exit 4, never a silent container) --- + _blocked() { bad "INSTRUMENT BLOCKED — local lane: $1"; shift; for _l in "$@"; do echo " $_l"; done + echo " Not falling back to a container on its own — that is your decision (MXTK_APP_LANE=container)." + echo " Record the run as INSTRUMENT BLOCKED, not as a failure."; exit 4; } + _JE="$(find_java_exe 2>/dev/null || true)"; _JW=21; command -v _mxtk_java_want >/dev/null 2>&1 && _JW="$(_mxtk_java_want)"; _JM="" + if [ -n "$_JE" ] && [ -x "$_JE" ]; then + # first run of digits in the version token: "21-ea", "21.0.2", "25", "1.8.0_392" all read; unreadable -> warn, go on + _JV="$("$_JE" -version 2>&1 | sed -n 's/.* version "\([^"]*\)".*/\1/p' | head -1)" + _JM="$(printf '%s' "$_JV" | sed -n 's/^[^0-9]*\([0-9][0-9]*\).*/\1/p')" + [ -n "$_JM" ] || warn "could not read the Java version from '$_JE -version' — continuing; this toolkit expects JDK $_JW for this model version" + fi + [ -n "$_JE" ] && [ -x "$_JE" ] || _blocked "no JDK found (this model needs JDK $_JW)." \ + "→ Install JDK $_JW and put it on PATH, or set JAVA_HOME (toolkit.env), then re-run." + [ -z "$_JM" ] || [ "$_JM" -ge "$_JW" ] 2>/dev/null || warn "JDK $_JM found; this toolkit expects JDK $_JW for this model version; continuing ($_JE)" + _DBM="${RUN_LOCAL_DB:-auto}"; _DBH="${RUN_LOCAL_DB_HOST:-127.0.0.1:5432}"; _DBARGS="" + case "$_DBM" in + auto|postgresql|ensure|hsqldb) ;; + *) echo "RUN_LOCAL_DB takes auto, postgresql, ensure or hsqldb (stack.conf)" >&2; exit 2 ;; + esac + # TCP probe of host:port (port 5432 when RUN_LOCAL_DB_HOST names only a host). Only for postgresql / ensure. + _pg_up() { local h="${_DBH%:*}" p="${_DBH##*:}" w=0 pid + case "$_DBH" in *:*) ;; *) h="$_DBH"; p=5432 ;; esac + h="${h#[}"; h="${h%]}" + ( exec 3<>"/dev/tcp/$h/$p" ) 2>/dev/null & pid=$! + while kill -0 "$pid" 2>/dev/null; do + [ "$w" -ge 3 ] && { kill "$pid" 2>/dev/null; return 1; }; sleep 1; w=$((w + 1)) + done + wait "$pid"; } + _DBUSED="" + case "$_DBM" in + auto) _DBUSED="the model's own settings (RUN_LOCAL_DB=auto: no database option passed, nothing switched)" ;; + hsqldb) _DBARGS="--db-type hsqldb"; _DBUSED="hsqldb, the built-in file database (RUN_LOCAL_DB=hsqldb; its data is separate from any Postgres data)" ;; + ensure) _DBARGS="--ensure-db"; _DBUSED="postgresql, created if missing (RUN_LOCAL_DB=ensure)" + _pg_up || warn "nothing answers on $_DBH yet — --ensure-db will try to provision it" ;; + postgresql) _pg_up || _blocked "RUN_LOCAL_DB=postgresql but nothing answers on $_DBH." \ + "→ Start Postgres, or RUN_LOCAL_DB=ensure to have mxcli provision it, or RUN_LOCAL_DB=hsqldb." + _DBUSED="postgresql on $_DBH (RUN_LOCAL_DB=postgresql)" ;; + esac + if [ "$_DBM" != auto ]; then + LOCAL_DB_MODE="$_DBM" + warn "local database mode: RUN_LOCAL_DB=$_DBM (set in .claude/loop/stack.conf; published as LOCAL_DB=$_DBM in stack.env)" + fi + [ -z "${RUN_LOCAL_DB_HOST:-}" ] || case "$_DBM" in ensure|postgresql) _DBARGS="$_DBARGS --db-host $RUN_LOCAL_DB_HOST" ;; esac + # A split model is snapshotted first, with the script exec.sh uses (one mechanism, one rotation). + if [ "$_U0" -gt 0 ]; then + if [ -f "$(dirname "${BASH_SOURCE[0]}")/snapshot-mpr.sh" ]; then + bash "$(dirname "${BASH_SOURCE[0]}")/snapshot-mpr.sh" >/dev/null 2>&1 \ + && ok "split model ($_U0 units) snapshotted to .mpr-snapshots/ before the start" \ + || warn "snapshot-mpr.sh failed — starting WITHOUT a snapshot of the $_U0-unit split model" + else warn "no snapshot-mpr.sh beside this script — starting without a snapshot of the $_U0-unit split model"; fi + fi echo "→ App is down. Starting it without a container: mxcli run --local --watch --detach (:$_LA, admin :$_LM, build :${_LS:-6543})..." [ "$_DK" = 3 ] || echo " (no container runtime answered; the local lane needs none)" + echo " database: $_DBUSED" T0=$(date +%s) - "$MXCLI" run --local --watch --detach -p "$MPR" --app-port "$_LA" --admin-port "$_LM" ${_LS:+--serve-port "$_LS"} >"$LOCAL_LOG" 2>&1 + # shellcheck disable=SC2086 + "$MXCLI" run --local --watch --detach -p "$MPR" --app-port "$_LA" --admin-port "$_LM" ${_LS:+--serve-port "$_LS"} $_DBARGS >"$LOCAL_LOG" 2>&1 LOCAL_RC=$? if [ $LOCAL_RC -ne 0 ]; then bad "mxcli run --local failed (rc=$LOCAL_RC) — see $LOCAL_LOG"; tail -20 "$LOCAL_LOG" - echo " A missing database is the usual cause: it must exist before the first run (createdb, or --db-type hsqldb)." + if grep -q "database not reachable" "$LOCAL_LOG" 2>/dev/null; then + case "$_DBM" in + auto) echo " The database is not reachable. Nothing was switched for you. Choose one in .claude/loop/stack.conf:" ;; + ensure) echo " The database is still not reachable with RUN_LOCAL_DB=ensure (is Postgres running on ${RUN_LOCAL_DB_HOST:-127.0.0.1:5432}?). Choose one in .claude/loop/stack.conf:" ;; + *) echo " The database is not reachable with RUN_LOCAL_DB=$_DBM. Choose one in .claude/loop/stack.conf:" ;; + esac + echo " RUN_LOCAL_DB=hsqldb the built-in file database; starts with no Postgres; its data is SEPARATE from any Postgres data" + echo " RUN_LOCAL_DB=ensure create the local Postgres role and database (Postgres must be running)" + echo " Or start the Postgres this model expects." + fi exit 1 fi _U1=$(find "$MODEL_DIR/mprcontents" -name '*.mxunit' 2>/dev/null | wc -l | tr -d ' ') if [ "$_U0" -gt 0 ] && [ "$_U1" -lt "$_U0" ]; then warn "this mxcli consolidated the split model ($_U0 → $_U1 mprcontents units): a build artifact." - warn " → never commit it; restore with: git checkout -- $(basename "$MPR") mprcontents/" + warn " → never commit it; restore with: git checkout -- $(basename "$MPR") mprcontents/ (or the newest .mpr-snapshots/ copy)" fi if prove_app; then APP_FOUND=0; record_served diff --git a/skills/checkpoints/checkpoint-cutover.md b/skills/checkpoints/checkpoint-cutover.md index e388a562..891016d6 100644 --- a/skills/checkpoints/checkpoint-cutover.md +++ b/skills/checkpoints/checkpoint-cutover.md @@ -81,6 +81,8 @@ somewhere the script does not look — write an inbox entry by hand. Record the `PROJECT.md` → `## Decisions` → `Cutover — learnings harvested:` (the PR link, or "nothing to harvest" with a sentence on why). +If a full `bin/gate-check.sh` run or `--closeout` prints "N toolkit/mxcli bug(s) logged but not filed", run `bin/file-bugs.sh --auto` (or `--links`): `--auto` only posts when `PROJECT.md` has `Bug filing: auto`, otherwise it shows what it would file. `--file` is human-only. + ## Skip Rule Per `checkpoint-template.md`: this checkpoint may only be skipped if all three answers are already recorded `CONFIRMED` in `PROJECT.md`. Being a `✋` gate, it never auto-skips on `ASSUMED` values. diff --git a/skills/harness-architecture.md b/skills/harness-architecture.md index 1d6df08b..f18a4c96 100644 --- a/skills/harness-architecture.md +++ b/skills/harness-architecture.md @@ -119,6 +119,10 @@ Every instrument's entire contract with its orchestrator is **an exit code plus | **1** | measured, wrong — the thing is broken | `fail` / finding | | **2** | **the instrument did not run** | `fault` | +`project-bin/exec.sh` has more codes (4, 5) whose meaning is its own, so it also prints one +`MXTK-RESULT exec verdict=... applied=... verified=... lint_new=... exit=...` line per run +(verdicts in its header): branch on `verdict=`, not on a code you must look up. Never the last line. + `fault` is **absent**, not amber. Amber means "ran, degraded". "The test could not run" and "the feature is broken" go to different people, and blending them is how a week gets spent debugging a page that was never reached. diff --git a/tests/wave2/fixtures/cold-start/after.txt b/tests/wave2/fixtures/cold-start/after.txt new file mode 100644 index 00000000..c6b6cd80 --- /dev/null +++ b/tests/wave2/fixtures/cold-start/after.txt @@ -0,0 +1,13 @@ +── Test stack: Fixture ────────────────────────────────── + ✓ Studio Pro not running + ✗ App not serving on any of: 8531 + ! Jaeger DOWN on :1 — Trace assertions unavailable (UI+Data still fine) + ✓ No mock API configured (set MOCK_HEALTH_URL in .claude/loop/stack.conf if this project has one) + ✓ split model (411 units) snapshotted to .mpr-snapshots/ before the start +→ App is down. Starting it without a container: mxcli run --local --watch --detach (:8531, admin :8532, build :6577)... + database: hsqldb, the built-in file database (no Postgres answers on 127.0.0.1:5432; RUN_LOCAL_DB=postgresql to require one) + ✓ App serving on :8531 in 46s (ownership verified, run-local) — --watch keeps it current +──────────────────────────────────────────────────────── +READY — ports published to .claude/loop/stack.env; the e2e config reads them automatically + app :8531 (ownership verified, run-local) + jaeger DOWN — Trace assertions will not run diff --git a/tests/wave2/fixtures/cold-start/before.txt b/tests/wave2/fixtures/cold-start/before.txt new file mode 100644 index 00000000..bf14db29 --- /dev/null +++ b/tests/wave2/fixtures/cold-start/before.txt @@ -0,0 +1,20 @@ +── Test stack: Fixture ────────────────────────────────── + ✓ Studio Pro not running + ✗ App not serving on any of: 8531 + ! Jaeger DOWN on :1 — Trace assertions unavailable (UI+Data still fine) + ✓ No mock API configured (set MOCK_HEALTH_URL in .claude/loop/stack.conf if this project has one) +→ App is down. Starting it without a container: mxcli run --local --watch --detach (:8531, admin :8532, build :6577)... + ✗ mxcli run --local failed (rc=1) — see /Fixture-run-local.log +WARNING: This is a vibe-coded PoC, alpha quality, use with caution. +failed: database not reachable at 127.0.0.1:5432: dial tcp 127.0.0.1:5432: connect: connection refused + last lines of .mxcli/run.log: + WARNING: This is a vibe-coded PoC, alpha quality, use with caution. + Warning: git branch "main" has no upstream. Studio Pro 11.13 fails to open a project in this state ("Unable to find 'system' property in 'system'"). Before opening it in Studio Pro, run: git push -u origin main + Configuration "Default" sets no constant values; using each constant's default. + Detecting project version... + Mendix version: 11.13.0 + Ensuring MxBuild and runtime are available... + MxBuild 11.13.0 already cached at /.mxcli/mxbuild/11.13.0/modeler/mxbuild + Error: database not reachable at 127.0.0.1:5432: dial tcp 127.0.0.1:5432: connect: connection refused + Pass --ensure-db to provision it, or start Postgres and create the 'fixture' database (user "mendix"). + A missing database is the usual cause: it must exist before the first run (createdb, or --db-type hsqldb). diff --git a/tests/wave2/fixtures/cold-start/java-version-21.txt b/tests/wave2/fixtures/cold-start/java-version-21.txt new file mode 100644 index 00000000..8e790002 --- /dev/null +++ b/tests/wave2/fixtures/cold-start/java-version-21.txt @@ -0,0 +1,4 @@ +Picked up JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS +openjdk version "21.0.12.1" 2026-08-18 +OpenJDK Runtime Environment (build 21.0.12.1+1-1-24.04.4-Ubuntu) +OpenJDK 64-Bit Server VM (build 21.0.12.1+1-1-24.04.4-Ubuntu, mixed mode, sharing) diff --git a/tests/wave2/fixtures/design-reaches-app/CAPTURE.md b/tests/wave2/fixtures/design-reaches-app/CAPTURE.md index aff0ab50..5c9c22d5 100644 --- a/tests/wave2/fixtures/design-reaches-app/CAPTURE.md +++ b/tests/wave2/fixtures/design-reaches-app/CAPTURE.md @@ -67,3 +67,14 @@ Measured there on 2026-09-28, with the built sheet passed by hand and nothing el 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. + +## #670 captures (2026-11) + +`real-ds.css`, `real-built-excerpt.css` and `real-custom-variables.scss` come from a fictional +visitor-pass test app (a throwaway test project, 2026-11), copied unedited. `real-ds.css` is its +`design/ds.css` whole (131 lines, several `--token:value;` per line). `real-custom-variables.scss` +is its `theme/web/custom-variables.scss` whole. `real-built-excerpt.css` is verbatim line ranges of +its `deployment/web/theme.compiled.css` (974 KB, too large to commit), concatenated in this order, +no edits: lines 6376-6380 (`@media` + `.lead`), 6718-6722 (`@media` + `.container`), 3397-3449 +(a bare `:root` carrying the design's tokens), 18118-18131 (the multi-line selector list +`.card, .mx-layoutgrid > ...`), 26662-26664 (`.card-body`). diff --git a/tests/wave2/fixtures/design-reaches-app/real-built-excerpt.css b/tests/wave2/fixtures/design-reaches-app/real-built-excerpt.css new file mode 100644 index 00000000..7a30e6bd --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/real-built-excerpt.css @@ -0,0 +1,80 @@ +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +:root { + /* ---- the one colour that defines the app --------------------------- */ + --mxt-brand: #2F5D62; + --mxt-brand-hover: #244A4E; + --mxt-brand-ink: #FFFFFF; + /* text on a brand-filled surface */ + /* ---- semantic status ----------------------------------------------- */ + --mxt-success: #2E7D5B; + --mxt-warning: #B7791F; + --mxt-danger: #B4412F; + --mxt-info: #3B6EA5; + /* ---- surfaces and ink ----------------------------------------------- */ + --mxt-ground: #F6F3EE; + /* the app background */ + --mxt-surface: #FFFFFF; + /* cards, modals, panels */ + --mxt-surface-alt: #FAF8F5; + /* striped rows, subtle fills */ + --mxt-surface-hover: #F1EEE8; + --mxt-surface-selected: #E6EFEF; + --mxt-ink: #1E2A32; + --mxt-ink-muted: #56636C; + --mxt-ink-faint: #8A949B; + --mxt-line: #E4DED5; + /* ---- the navigation rail -------------------------------------------- */ + --mxt-rail: #243138; + --mxt-rail-line: #34454D; + --mxt-rail-ink: #BCC8CD; + --mxt-rail-ink-active: #FFFFFF; + /* ---- status pill tones ---------------------------------------------- */ + --mxt-tint-ok: #E3F1EA; + --mxt-tone-ok: #1F5C41; + --mxt-tint-warn: #FBEFD9; + --mxt-tone-warn: #7A4E0C; + --mxt-tint-risk: #F8E4DF; + --mxt-tone-risk: #8A2E1F; + --mxt-tint-info: #E3ECF5; + --mxt-tone-info: #274D78; + --mxt-tint-neutral: #ECE9E3; + /* ---- shape, density, elevation --------------------------------------- */ + --mxt-radius: 8px; + --mxt-radius-lg: 14px; + --mxt-row-height: 56px; + --mxt-control-height: 40px; + --mxt-shadow: 0 1px 2px rgba(30,42,50,.06), 0 4px 14px rgba(30,42,50,.06); + --mxt-focus-halo: 0 0 0 3px rgba(15, 110, 107, 0.16); + /* ---- type ------------------------------------------------------------ */ + --mxt-font: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mxt-font-heading: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mxt-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace; + --mxt-font-size: 15px; + --mxt-line-height: 1.5; +} + Cards +========================================================================== */ +.card, .mx-layoutgrid > .row.cards > .col, +.mx-layoutgrid > .row.cards > [class*=col-], .mx-layoutgrid.cards > .row > .col, +.mx-layoutgrid.cards > .row > [class*=col-] { + border: var(--card-border); + border-radius: var(--card-border-radius); + background-color: var(--card-bg); + overflow: hidden; + position: relative; + padding: var(--card-padding); + box-shadow: var(--card-shadow); + margin-bottom: var(--card-margin-bottom); +} +.card-body { + padding: var(--spacing-large); +} diff --git a/tests/wave2/fixtures/design-reaches-app/real-custom-variables.scss b/tests/wave2/fixtures/design-reaches-app/real-custom-variables.scss new file mode 100644 index 00000000..cee98355 --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/real-custom-variables.scss @@ -0,0 +1,676 @@ +// Theme options +$brand-logo: false; +$font-family-import: false; +$form-input-style: default; +$btn-bordered: false; // Default value false, set to true if you want this effect + +:root { + /* Brand Colors */ + --brand-primary: #264ae5; + --brand-success: #16aa16; + --brand-warning: #cd8501; + --brand-danger: #EA3337; + --brand-default: color-mix(in srgb, var(--brand-primary) 10%, #e7e7e9); + --gray: #B8BABF; + +// /* Hover Color Variants */ +// --brand-default-hover: var(--brand-default-100); +// --brand-primary-hover: var(--brand-primary-700); +// --brand-success-hover: var(--brand-success-700); +// --brand-warning-hover: var(--brand-warning-700); +// --brand-danger-hover: var(--brand-danger-700); + +// /* Brand Logo */ +// --brand-logo-height: 24px; +// --brand-logo-width: 32px; +// --brand-gradient: linear-gradient(to bottom, var(--brand-primary-900), var(--brand-primary-700)); + +// /* Default Font Size & Color */ +// --font-size-default: 14px; +// --font-color-default: var(--gray-900); +// --font-color-contrast: var(--color-base); + +// /* Global Border */ +// --border-color-default: var(--brand-default-300); +// --border-radius-s: 4px; +// --border-radius-m: 8px; +// --border-radius-l: 12px; +// --border-radius-default: var(--border-radius-s); +// --border-width-thin: 1px; +// --border-width-medium: 2px; +// --border-width-thick: 3px; +// --border-width-default: var(--border-width-thin); +// --border-default: var(--border-width-default) solid var(--border-color-default); + +// /* Focus indicators for accessibility */ +// --focus-outline: 2px solid var(--form-input-border-focus-color); +// --focus-outline-offset: 2px; + +// /* Topbar */ +// --topbar-bg: linear-gradient(to bottom, var(--brand-primary-500), var(--brand-primary-600)); +// --topbar-minimalheight: 48px; +// --topbar-border-color: var(--border-color-default); + +// /* Sidebar */ +// --sidebar-bg: linear-gradient(to bottom, var(--brand-primary-600), var(--brand-primary-700)); + +// /* Topbar mobile */ +// --m-header-height: 45px; +// --m-header-bg: var(--topbar-bg); +// --m-header-color: var(--font-color-contrast); +// --m-header-title-size: 16px; + +// /* Navbar Brand Name */ +// --navbar-brand-name: var(--font-color-default); + +// /* Background Colors */ +// --bg-color: var(--brand-default-100); +// --bg-color-secondary: var(--color-base); + +// /* Default Link Color */ +// --link-color: var(--brand-primary); +// --link-hover-color: var(--brand-primary-400); + +// /* Font Family */ +// --font-family-base: "Poppins", sans-serif; + +// /* Font Sizes */ +// --font-size-large: 18px; +// --font-size-small: 12px; + +// /* Font Weights */ +// --font-weight-light: 300; +// --font-weight-normal: 400; +// --font-weight-semibold: 600; +// --font-weight-bold: 700; + +// /* Header Font Sizes */ +// --font-size-h1: 34px; +// --font-size-h2: 26px; +// --font-size-h3: 24px; +// --font-size-h4: 16px; +// --font-size-h5: var(--font-size-default); +// --font-size-h6: 12px; + +// /* Header Font Weight */ +// --font-weight-header: var(--font-weight-semibold); + +// /* Line Height */ +// --line-height-base: 1.428571429; + +// /* Header Margin */ +// --font-header-margin: 0 0 8px 0; + +// /* Font Colors */ +// --font-color-detail: var(--brand-primary-700); +// --font-color-header: var(--brand-primary-800); + +// /* Navigation */ +// --navigation-item-height: unset; +// --navigation-item-padding: var(--spacing-medium); +// --navigation-border-radius: var(--border-radius-m); +// --navigation-scrollbar-color: rgba(255, 255, 255, 0.5) transparent; + +// --navigation-font-size: var(--font-size-default); +// --navigation-sub-font-size: var(--font-size-small); +// --navigation-glyph-size: 20px; + +// --navigation-color: var(--font-color-contrast); +// --navigation-color-hover: var(--font-color-contrast); +// --navigation-color-active: var(--font-color-contrast); + +// --navigation-sub-color: var(--navigation-color); +// --navigation-sub-color-hover: var(--navigation-color-hover); +// --navigation-sub-color-active: var(--navigation-color-active); + +// /* Sidebar Navigation */ +// --navigation-bg: var(--topbar-bg); +// --navigation-bg-hover: rgba(0, 0, 0, 0.2); +// --navigation-bg-active: rgba(0, 0, 0, 0.3); + +// --navigation-sub-bg: rgba(0, 0, 0, 0.2); +// --navigation-sub-bg-hover: var(--navigation-bg-hover); +// --navigation-sub-bg-active: var(--navigation-bg-active); + +// --navigation-border-color: var(--navigation-bg-hover); + +// --navsidebar-bg: var(--sidebar-bg); +// --navsidebar-bg-hover: var(--navigation-bg-hover); +// --navsidebar-bg-active: var(--navigation-bg-active); + +// --navsidebar-sub-bg: rgba(0, 0, 0, 0.2); +// --navsidebar-sub-bg-hover: var(--navigation-bg-hover); +// --navsidebar-sub-bg-active: var(--navigation-bg-active); +// --navsidebar-sub-bg-header: rgba(0, 0, 0, 0.2); +// --navsidebar-sub-bg-collapsed: var(--brand-primary-700); + +// --navsidebar-border-color: var(--border-color-default); +// --navsidebar-border-radius: var(--navigation-border-radius); +// --navsidebar-shadow: 0 0 4px rgb(0 0 0 / 14%), 2px 4px 8px rgb(0 0 0 / 28%); + +// --navsidebar-font-size: var(--font-size-default); +// --navsidebar-sub-font-size: var(--font-size-small); +// --navsidebar-glyph-size: 20px; + +// --navsidebar-color: var(--navigation-color); +// --navsidebar-color-hover: var(--navigation-color-hover); +// --navsidebar-color-active: var(--navigation-color-active); + +// --navsidebar-sub-color: var(--navigation-color); +// --navsidebar-sub-color-hover: var(--navigation-color-hover); +// --navsidebar-sub-color-active: var(--navigation-color-active); + +// --navsidebar-width-closed: 48px; +// --navsidebar-width-open: 232px; +// --navsidebar-toggle-size: 32px; + +// --navsidebar-icon-height: 48px; +// --navsidebar-icon-width: 48px; +// --navsidebar-animation-duration: 250ms; +// --navsidebar-animation-function: ease-in; + +// /* Topbar Navigation */ +// --navtopbar-font-size: var(--font-size-small); +// --navtopbar-sub-font-size: var(--font-size-small); +// --navtopbar-glyph-size: 1.2em; + +// --navtopbar-bg: var(--topbar-bg); +// --navtopbar-bg-hover: var(--navigation-bg-hover); +// --navtopbar-bg-active: var(--navigation-bg-active); +// --navtopbar-color: var(--navigation-color); +// --navtopbar-color-hover: var(--navigation-color-hover); +// --navtopbar-color-active: var(--navigation-color-active); + +// --navtopbar-sub-bg: var(--navtopbar-bg); +// --navtopbar-sub-bg-hover: var(--navtopbar-bg-hover); +// --navtopbar-sub-bg-active: var(--navtopbar-bg-active); +// --navtopbar-sub-color: var(--navigation-color); +// --navtopbar-sub-color-hover: var(--navigation-color-hover); +// --navtopbar-sub-color-active: var(--navigation-color-active); + +// --navtopbar-border-color: var(--topbar-border-color); +// --navtopbar-border-radius: var(--navigation-border-radius); + +// /* Shadows */ +// --shadow-color-border: var(--border-color-default); +// --shadow-color: var(--brand-default-300); +// --shadow-small: 0 2px 4px 0; +// --shadow-medium: 0 4px 6px 0; +// --shadow-large: 0 8px 10px 0; + +// /* Form Inputs */ +// --form-label-color: var(--font-color-default); +// --form-label-size: var(--font-size-default); +// --form-label-weight: var(--font-weight-normal); +// --form-label-gutter: 8px; + +// --form-input-height: auto; +// --form-input-padding-y: 8px; +// --form-input-padding-x: 8px; +// --form-input-static-padding-y: 8px; +// --form-input-static-padding-x: 0; +// --form-input-font-size: var(--form-label-size); +// --form-input-line-height: var(--line-height-base); +// --form-input-border-radius: 6px; + +// --form-input-bg: var(--color-base); +// --form-input-bg-focus: var(--color-base); +// --form-input-bg-hover: var(--color-base); +// --form-input-bg-disabled: var(--bg-color); +// --form-input-color: var(--font-color-default); +// --form-input-focus-color: var(--form-input-color); +// --form-input-disabled-color: var(--gray-700); +// --form-input-placeholder-color: var(--gray-600); +// --form-input-border-color: var(--brand-default); +// --form-input-border-focus-color: var(--brand-primary); +// --form-input-border-hover-color: color-mix(in srgb, var(--form-input-border-color), var(--form-input-border-focus-color) 50%); + +// --form-input-static-border-color: var(--gray-200); + +// --form-group-margin-bottom: 16px; +// --form-group-gutter: 16px; + +// /* Buttons */ +// --btn-font-size: var(--font-size-default); +// --btn-border-radius: var(--border-radius-default); + +// --btn-default-bg: var(--color-base); +// --btn-primary-bg: var(--brand-primary-600); +// --btn-success-bg: var(--brand-success-600); +// --btn-warning-bg: var(--brand-warning-600); +// --btn-danger-bg: var(--brand-danger-600); + +// --btn-default-border-color: var(--brand-default); +// --btn-primary-border-color: var(--brand-primary-600); +// --btn-success-border-color: var(--brand-success-600); +// --btn-warning-border-color: var(--brand-warning-600); +// --btn-danger-border-color: var(--brand-danger-600); + +// --btn-default-color: var(--brand-primary); +// --btn-primary-color: var(--font-color-contrast); +// --btn-success-color: var(--font-color-contrast); +// --btn-warning-color: var(--font-color-contrast); +// --btn-danger-color: var(--font-color-contrast); + +// --btn-default-icon-color: var(--gray); + +// --btn-default-bg-hover: var(--brand-default-hover); +// --btn-primary-bg-hover: var(--brand-primary-hover); +// --btn-success-bg-hover: var(--brand-success-hover); +// --btn-warning-bg-hover: var(--brand-warning-hover); +// --btn-danger-bg-hover: var(--brand-danger-hover); +// --btn-link-bg-hover: var(--brand-default-hover); + +// --btn-default-bg-active: var(--brand-default-300); +// --btn-primary-bg-active: var(--brand-primary-500); +// --btn-success-bg-active: var(--brand-success-500); +// --btn-warning-bg-active: var(--brand-warning-500); +// --btn-danger-bg-active: var(--brand-danger-500); +// --btn-link-bg-active: var(--brand-default-500); + +// /* Header */ +// --header-min-height: 240px; +// --header-bg-color: var(--brand-primary); +// --header-bgimage-filter: brightness(60%); +// --header-text-color: var(--color-base); +// --header-text-color-detail: rgba(0, 0, 0, 0.2); + +// /* Grid */ +// --grid-border-color: var(--border-color-default); + +// --grid-bg: transparent; +// --grid-bg-header: transparent; +// --grid-bg-hover: var(--brand-default-hover); +// --grid-bg-selected: var(--brand-default); +// --grid-bg-selected-hover: var(--brand-default-600); +// --grid-bg-striped: var(--brand-default-50); +// --grid-footer-bg: var(--brand-default); + +// --grid-selected-color: var(--font-color-default); + +// --grid-paging-bg: transparent; +// --grid-paging-bg-hover: transparent; +// --grid-paging-border-color: transparent; +// --grid-paging-border-color-hover: transparent; +// --grid-paging-color: var(--gray-300); +// --grid-paging-color-hover: var(--brand-primary); + +// /* Tabs */ +// --tabs-color: var(--font-color-default); +// --tabs-color-active: var(--font-color-detail); +// --tabs-lined-color-active: var(--font-color-detail); + +// --tabs-lined-border-width: 3px; +// --tabs-border-color: var(--border-color-default); +// --tabs-lined-border-color: var(--brand-primary); +// --tabs-bg: transparent; +// --tabs-bg-pills: var(--brand-default-200); +// --tabs-bg-hover: var(--brand-default-300); +// --tabs-bg-active: var(--brand-primary-600); + +// /* Modal */ +// --modal-header-bg: transparent; +// --modal-header-border-color: var(--border-color-default); +// --modal-header-color: var(--font-color-header); +// --modal-body-bg: var(--bg-color-secondary); +// --modal-footer-bg: var(--bg-color-secondary); + +// /* Data View */ +// --dataview-controls-bg: inherit; +// --dataview-controls-border-color: var(--border-color-default); +// --dataview-controls-alignment: left; + +// --dataview-emptymessage-bg: var(--bg-color); +// --dataview-emptymessage-color: var(--font-color-default); + +// /* Cards */ +// --card-shadow: var(--shadow-small) var(--shadow-color); +// --card-border: var(--border-default); +// --card-border-radius: var(--border-radius-m); +// --card-padding: var(--spacing-large); +// --card-margin-bottom: var(--spacing-smaller); +// --card-bg: var(--bg-color-secondary); + +// /* Alerts */ +// --alert-primary-bg: var(--brand-primary-50); +// --alert-success-bg: var(--brand-success-50); +// --alert-warning-bg: var(--brand-warning-50); +// --alert-danger-bg: var(--brand-danger-50); + +// --alert-primary-color: var(--brand-primary-700); +// --alert-success-color: var(--brand-success-700); +// --alert-warning-color: var(--brand-warning-700); +// --alert-danger-color: var(--brand-danger-700); + +// --alert-primary-border-color: var(--brand-primary-600); +// --alert-success-border-color: var(--brand-success-600); +// --alert-warning-border-color: var(--brand-warning-600); +// --alert-danger-border-color: var(--brand-danger-600); + +// /* Wizard */ +// --wizard-step-height: 48px; +// --wizard-step-number-size: 64px; +// --wizard-step-number-font-size: var(--font-size-h3); + +// --wizard-default-bg: var(--color-base); +// --wizard-default-color: var(--color-base); +// --wizard-default-step-color: var(--font-color-default); +// --wizard-default-border-color: var(--border-color-default); + +// --wizard-active-bg: var(--brand-primary-200); +// --wizard-active-color: var(--brand-primary-700); +// --wizard-active-step-color: var(--brand-primary-700); +// --wizard-active-border-color: var(--brand-primary-700); + +// --wizard-visited-bg: var(--brand-success-200); +// --wizard-visited-color: var(--brand-success-700); +// --wizard-visited-step-color: var(--brand-success-700); +// --wizard-visited-border-color: var(--brand-success-700); + +// /* Labels */ +// --label-default-bg: var(--brand-default); +// --label-primary-bg: var(--brand-primary-600); +// --label-success-bg: var(--brand-success-600); +// --label-warning-bg: var(--brand-warning-600); +// --label-danger-bg: var(--brand-danger-600); + +// --label-default-border-color: var(--brand-default-600); +// --label-primary-border-color: var(--brand-primary-600); +// --label-success-border-color: var(--brand-success-600); +// --label-warning-border-color: var(--brand-warning-600); +// --label-danger-border-color: var(--brand-danger-600); + +// --label-default-color: var(--font-color-default); +// --label-primary-color: var(--color-base); +// --label-success-color: var(--color-base); +// --label-warning-color: var(--color-base); +// --label-danger-color: var(--color-base); + +// /* Groupbox */ +// --groupbox-default-bg: var(--brand-default); +// --groupbox-primary-bg: var(--brand-primary-600); +// --groupbox-success-bg: var(--brand-success-600); +// --groupbox-warning-bg: var(--brand-warning-600); +// --groupbox-danger-bg: var(--brand-danger-600); +// --groupbox-white-bg: var(--color-base-600); + +// --groupbox-default-color: var(--font-color-default); +// --groupbox-primary-color: var(--font-color-contrast); +// --groupbox-success-color: var(--font-color-contrast); +// --groupbox-warning-color: var(--font-color-contrast); +// --groupbox-danger-color: var(--font-color-contrast); +// --groupbox-white-color: var(--font-color-default); + +// /* Callouts */ +// --callout-primary-color: var(--brand-primary-600); +// --callout-default-color: var(--font-color-default); +// --callout-success-color: var(--brand-success-700); +// --callout-warning-color: var(--brand-warning-700); +// --callout-danger-color: var(--brand-danger-600); + +// --callout-primary-bg: var(--brand-primary-100); +// --callout-default-bg: var(--gray-50); +// --callout-success-bg: var(--brand-success-100); +// --callout-warning-bg: var(--brand-warning-100); +// --callout-danger-bg: var(--brand-danger-100); + +// /* Timeline */ +// --timeline-icon-color: var(--brand-primary); +// --timeline-border-color: var(--border-color-default); +// --timeline-event-time-color: var(--brand-primary); +// --timeline-icon-size: 18px; +// --timeline-image-size: 36px; +// --timeline-grouping-size: 120px; +// --timeline-grouping-border-radius: 30px; +// --timeline-grouping-border-color: var(--timeline-border-color); + +// /* Accordion */ +// --accordion-header-default-bg: var(--bg-color-secondary); +// --accordion-header-default-bg-hover: var(--brand-default-hover); +// --accordion-header-default-color: var(--font-color-header); +// --accordion-default-border-color: var(--border-color-default); +// --accordion-bg-striped: var(--brand-default-200); +// --accordion-bg-striped-hover: var(--brand-default-hover); + +// --accordion-header-primary-bg: var(--brand-primary-600); +// --accordion-header-secondary-bg: var(--bg-color-secondary); +// --accordion-header-success-bg: var(--brand-success-600); +// --accordion-header-warning-bg: var(--brand-warning-600); +// --accordion-header-danger-bg: var(--brand-danger-600); + +// --accordion-header-primary-bg-hover: var(--brand-primary-hover); +// --accordion-header-secondary-bg-hover: var(---brand-default-hover); +// --accordion-header-success-bg-hover: var(--brand-success-hover); +// --accordion-header-warning-bg-hover: var(--brand-warning-hover); +// --accordion-header-danger-bg-hover: var(--brand-danger-hover); + +// --accordion-header-primary-color: var(--font-color-contrast); +// --accordion-header-secondary-color: var(--brand-primary); +// --accordion-header-success-color: var(--font-color-contrast); +// --accordion-header-warning-color: var(--font-color-contrast); +// --accordion-header-danger-color: var(--font-color-contrast); + +// --accordion-primary-border-color: var(--brand-primary); +// --accordion-secondary-border-color: var(--gray-200); +// --accordion-success-border-color: var(--brand-success); +// --accordion-warning-border-color: var(--brand-warning); +// --accordion-danger-border-color: var(--brand-danger); + +// /* Spacing */ +// --spacing-none: 0px; +// --spacing-smallest: 2px; +// --spacing-smaller: 4px; +// --spacing-small: 8px; +// --spacing-medium: 16px; + +// --t-spacing-medium: 16px; +// --m-spacing-medium: 16px; + +// --spacing-large: 24px; +// --t-spacing-large: 24px; +// --m-spacing-large: 16px; + +// --spacing-larger: 32px; +// --spacing-largest: 48px; + +// --layout-spacing-top: 24px; +// --layout-spacing-right: 24px; +// --layout-spacing-bottom: 24px; +// --layout-spacing-left: 24px; + +// --t-layout-spacing-top: 24px; +// --t-layout-spacing-right: 24px; +// --t-layout-spacing-bottom: 24px; +// --t-layout-spacing-left: 24px; + +// --m-layout-spacing-top: 16px; +// --m-layout-spacing-right: 16px; +// --m-layout-spacing-bottom: 16px; +// --m-layout-spacing-left: 16px; + +// --layout-spacing: var(--layout-spacing-top) var(--layout-spacing-right) var(--layout-spacing-bottom) var(--layout-spacing-left); +// --m-layout-spacing: var(--m-layout-spacing-top) var(--m-layout-spacing-right) var(--m-layout-spacing-bottom) var(--m-layout-spacing-left); +// --t-layout-spacing: var(--t-layout-spacing-top) var(--t-layout-spacing-right) var(--t-layout-spacing-bottom) var(--t-layout-spacing-left); + +// --gutter-size: var(--spacing-medium); + +// --padding-table-cell-top: 8px; +// --padding-table-cell-bottom: 8px; +// --padding-table-cell-left: 8px; +// --padding-table-cell-right: 8px; + +// /* Brand Variants */ +// --btn-inverse-bg: var(--brand-primary-400); +// --btn-info-bg: var(--brand-primary-300); +// --btn-inverse-border-color: var(--brand-primary-400); +// --btn-info-border-color: var(--brand-primary-300); + +// --btn-inverse-color: var(--color-base); +// --btn-info-color: var(--color-base); + +// --btn-inverse-bg-hover: var(--brand-primary-300); +// --btn-info-bg-hover: var(--brand-primary-300); + +// /* Alerts */ +// --alert-info-bg: var(--brand-primary-50); +// --alert-info-color: var(--brand-primary-600); +// --alert-info-border-color: var(--brand-primary); + +// /* Labels */ +// --label-info-bg: var(--brand-primary-300); +// --label-inverse-bg: var(--brand-primary-600); + +// --label-info-border-color: var(--brand-primary-300); +// --label-inverse-border-color: var(--brand-primary-600); + +// --label-info-color: var(--color-base); +// --label-inverse-color: var(--color-base); + +// /* Groupbox */ +// --groupbox-inverse-bg: var(--brand-primary-600); +// --groupbox-info-bg: var(--brand-primary-300); + +// --groupbox-inverse-color: var(--color-base); +// --groupbox-info-color: var(--color-base); + +// /* Callouts */ +// --callout-info-color: var(--brand-primary-300); +// --callout-info-bg: var(--brand-primary-50); + +// --screen-xs: #{$screen-xs}; +// --screen-sm: #{$screen-sm}; +// --screen-md: #{$screen-md}; +// --screen-lg: #{$screen-lg}; +// --screen-xl: #{$screen-xl}; +// --screen-xxl: #{$screen-xxl}; + +// /* Legacy variables */ +// --gray-lighter: var(--gray-50); +// --gray-light: var(--gray-300); +// --gray-primary: var(--gray-200); +// --gray-dark: var(--gray-700); +// --gray-darker: var(--gray-800); + +// /* Color variations */ +// --color-default-darker: var(--gray-700); +// --color-default-dark: var(--gray-600); +// --color-default-light: var(--gray-100); +// --color-default-lighter: var(--gray-50); + +// --color-primary-darker: var(--brand-primary-700); +// --color-primary-dark: var(--brand-primary-600); +// --color-primary-light: var(--brand-primary-100); +// --color-primary-lighter: var(--brand-primary-50); + +// --color-success-darker: var(--brand-success-700); +// --color-success-dark: var(--brand-success-600); +// --color-success-light: var(--brand-success-100); +// --color-success-lighter: var(--brand-success-50); + +// --color-warning-darker: var(--brand-warning-700); +// --color-warning-dark: var(--brand-warning-600); +// --color-warning-light: var(--brand-warning-100); +// --color-warning-lighter: var(--brand-warning-50); + +// --color-danger-darker: var(--brand-danger-700); +// --color-danger-dark: var(--brand-danger-600); +// --color-danger-light: var(--brand-danger-100); +// --color-danger-lighter: var(--brand-danger-50); + +// --grid-padding-top: 16px; +// --grid-padding-right: 16px; +// --grid-padding-bottom: 16px; +// --grid-padding-left: 16px; + +// --listview-padding-top: 16px; +// --listview-padding-right: 16px; +// --listview-padding-bottom: 16px; +// --listview-padding-left: 16px; +} + + +//== Media queries breakpoints +//## Define the breakpoints at which your layout will change, adapting to different screen sizes. +// $screen-xs: 480px; +// $screen-sm: 576px; +// $screen-md: 768px; +// $screen-lg: 992px; +// $screen-xl: 1200px; +// $screen-xxl: 1400px; + +// Ensures backwards compatibility. Set to false if the theme variables is not using CSS variables (e.g. --brand-primary). +$use-css-variables: true; +@import "../../themesource/atlas_core/web/variables"; + +// mxcli:theme:begin frontdesk v1 — generated by `mxcli theme apply`; edit outside this block +// Layer 1 — the Frontdesk palette. This is the file to edit. +// +// These are the theme's own semantic tokens. The next file down +// (_mxcli-frontdesk.scss) maps them onto the ~60 Atlas variables that Atlas +// components, form controls and the brand-aware pluggable widgets (Switch, +// Slider, ProgressBar, BadgeButton) actually read — so changing --mxt-brand +// here re-brands the entire app, with no per-widget CSS. +// +// The values below are the LIGHT palette, which is Frontdesk's default. The dark +// palette lives beside the light-to-Atlas mapping in _mxcli-frontdesk.scss. +// +// Mendix 11 Atlas is CSS-custom-property-first, so these are `:root` +// declarations rather than SCSS `!default` variables. This file is imported +// once per module, so it must hold declarations only — never rules. + +:root { + /* ---- the one colour that defines the app --------------------------- */ + --mxt-brand: #2F5D62; + --mxt-brand-hover: #244A4E; + --mxt-brand-ink: #FFFFFF; /* text on a brand-filled surface */ + + /* ---- semantic status ----------------------------------------------- */ + --mxt-success: #2E7D5B; + --mxt-warning: #B7791F; + --mxt-danger: #B4412F; + --mxt-info: #3B6EA5; + + /* ---- surfaces and ink ----------------------------------------------- */ + --mxt-ground: #F6F3EE; /* the app background */ + --mxt-surface: #FFFFFF; /* cards, modals, panels */ + --mxt-surface-alt: #FAF8F5; /* striped rows, subtle fills */ + --mxt-surface-hover: #F1EEE8; + --mxt-surface-selected: #E6EFEF; + --mxt-ink: #1E2A32; + --mxt-ink-muted: #56636C; + --mxt-ink-faint: #8A949B; + --mxt-line: #E4DED5; + + /* ---- the navigation rail -------------------------------------------- */ + --mxt-rail: #243138; + --mxt-rail-line: #34454D; + --mxt-rail-ink: #BCC8CD; + --mxt-rail-ink-active: #FFFFFF; + + /* ---- status pill tones ---------------------------------------------- */ + --mxt-tint-ok: #E3F1EA; + --mxt-tone-ok: #1F5C41; + --mxt-tint-warn: #FBEFD9; + --mxt-tone-warn: #7A4E0C; + --mxt-tint-risk: #F8E4DF; + --mxt-tone-risk: #8A2E1F; + --mxt-tint-info: #E3ECF5; + --mxt-tone-info: #274D78; + --mxt-tint-neutral: #ECE9E3; + + /* ---- shape, density, elevation --------------------------------------- */ + --mxt-radius: 8px; + --mxt-radius-lg: 14px; + --mxt-row-height: 56px; + --mxt-control-height: 40px; + --mxt-shadow: 0 1px 2px rgba(30,42,50,.06), 0 4px 14px rgba(30,42,50,.06); + --mxt-focus-halo: 0 0 0 3px rgba(15, 110, 107, 0.16); + + /* ---- type ------------------------------------------------------------ */ + --mxt-font: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mxt-font-heading: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mxt-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace; + --mxt-font-size: 15px; + --mxt-line-height: 1.5; +} +// mxcli:theme:end frontdesk 5b70ac3b9d7edcf7 diff --git a/tests/wave2/fixtures/design-reaches-app/real-ds.css b/tests/wave2/fixtures/design-reaches-app/real-ds.css new file mode 100644 index 00000000..10593ab5 --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/real-ds.css @@ -0,0 +1,131 @@ +/* ds.css — Visitor passes design system (FrontDesk). Tokens come from mxt-tokens.css (same values, inlined here + so every wireframe links ONE file). Never redeclare a token in a wireframe. */ +:root { + --mxt-brand:#2F5D62; --mxt-brand-hover:#244A4E; --mxt-brand-ink:#fff; + --mxt-ground:#F6F3EE; --mxt-surface:#fff; --mxt-surface-alt:#FAF8F5; --mxt-surface-hover:#F1EEE8; --mxt-surface-selected:#E6EFEF; + --mxt-ink:#1E2A32; --mxt-ink-muted:#56636C; --mxt-ink-faint:#8A949B; --mxt-line:#E4DED5; + --mxt-success:#2E7D5B; --mxt-warning:#B7791F; --mxt-danger:#B4412F; --mxt-info:#3B6EA5; + --mxt-tint-ok:#E3F1EA; --mxt-tint-warn:#FBEFD9; --mxt-tint-risk:#F8E4DF; --mxt-tint-info:#E3ECF5; --mxt-tint-neutral:#ECE9E3; + --mxt-tone-ok:#1F5C41; --mxt-tone-warn:#7A4E0C; --mxt-tone-risk:#8A2E1F; --mxt-tone-info:#274D78; + --mxt-rail:#243138; --mxt-rail-ink:#BCC8CD; --mxt-rail-line:#34454D; + --mxt-radius:8px; --mxt-radius-lg:14px; --mxt-shadow:0 1px 2px rgba(30,42,50,.06),0 4px 14px rgba(30,42,50,.06); + --mxt-font:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; + --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; +} +*{box-sizing:border-box} +body{margin:0;background:#E9E5DE;color:var(--mxt-ink);font:15px/1.5 var(--mxt-font)} +.sheet{padding:24px;display:flex;flex-direction:column;gap:28px} +.note{font-size:13px;color:var(--mxt-ink-muted);margin:0 0 6px} +.statelabel{font-weight:600;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--mxt-ink-muted);margin:0 0 8px} +.frames{display:flex;gap:28px;align-items:flex-start;flex-wrap:wrap} +.frame{background:var(--mxt-ground);border:1px solid var(--mxt-line);border-radius:12px;overflow:hidden;box-shadow:var(--mxt-shadow)} +.frame.desk{width:1180px;max-width:100%} +.frame.phone{width:390px;max-width:100%} +/* shell */ +.app{display:grid;grid-template-columns:220px 1fr;min-height:560px} +.phone .app{grid-template-columns:1fr} +.rail{background:var(--mxt-rail);color:var(--mxt-rail-ink);padding:20px 12px;display:flex;flex-direction:column;gap:4px} +.phone .rail{display:none} +.brand{color:#fff;font-weight:600;font-size:17px;padding:4px 10px 16px;letter-spacing:.01em} +.brand small{display:block;font-weight:400;font-size:12px;color:var(--mxt-rail-ink)} +.nav{display:block;padding:10px 12px;border-radius:var(--mxt-radius);color:var(--mxt-rail-ink);text-decoration:none;font-size:14px} +.nav.active{background:rgba(255,255,255,.1);color:#fff;font-weight:600} +.top{display:flex;align-items:center;justify-content:space-between;padding:14px 24px;background:var(--mxt-surface);border-bottom:1px solid var(--mxt-line)} +.phone .top{padding:12px 16px} +.who{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--mxt-ink-muted)} +.who b{color:var(--mxt-ink);font-weight:600} +.avatar{width:30px;height:30px;border-radius:50%;background:var(--mxt-surface-selected);color:var(--mxt-brand);display:grid;place-items:center;font-weight:600;font-size:12px} +.link{color:var(--mxt-brand);text-decoration:none;font-weight:600;font-size:13px} +.bottombar{display:none} +.phone .bottombar{display:flex;background:var(--mxt-surface);border-top:1px solid var(--mxt-line);position:sticky;bottom:0} +.phone .bottombar a{flex:1;text-align:center;padding:12px 4px;font-size:12px;color:var(--mxt-ink-muted);text-decoration:none} +.phone .bottombar a.active{color:var(--mxt-brand);font-weight:600;box-shadow:inset 0 2px 0 var(--mxt-brand)} +.main{padding:24px;display:flex;flex-direction:column;gap:20px} +.phone .main{padding:16px;gap:14px} +.head{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap} +h1{font-size:24px;line-height:1.2;margin:0;font-weight:600;letter-spacing:-.01em} +.phone h1{font-size:20px} +.sub{color:var(--mxt-ink-muted);font-size:14px;margin-top:2px} +/* controls */ +.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:40px;padding:0 16px;border-radius:var(--mxt-radius);border:1px solid var(--mxt-line);background:var(--mxt-surface);color:var(--mxt-ink);font:600 14px var(--mxt-font);cursor:pointer;text-decoration:none} +.btn.primary{background:var(--mxt-brand);border-color:var(--mxt-brand);color:var(--mxt-brand-ink)} +.btn.primary:hover{background:var(--mxt-brand-hover)} +.btn.big{height:44px;padding:0 22px;font-size:15px} +.btn.quiet{border-color:transparent;background:transparent;color:var(--mxt-ink-muted)} +.btn.danger{color:var(--mxt-danger);border-color:#E7C3BB} +.btn.block{width:100%} +.btn[disabled],.btn.off{opacity:.45;pointer-events:none} +.field{display:flex;flex-direction:column;gap:6px;margin-bottom:14px} +.field label{font-size:13px;font-weight:600;color:var(--mxt-ink)} +.field .in{height:40px;border:1px solid #CFC8BD;border-radius:var(--mxt-radius);background:#fff;padding:0 12px;display:flex;align-items:center;color:var(--mxt-ink)} +.field .in.ph{color:var(--mxt-ink-faint)} +.field .hint{font-size:12px;color:var(--mxt-ink-muted)} +.field.err .in{border-color:var(--mxt-danger);box-shadow:0 0 0 3px var(--mxt-tint-risk)} +.msg{display:flex;gap:8px;align-items:flex-start;padding:10px 12px;border-radius:var(--mxt-radius);font-size:14px} +.msg.err{background:var(--mxt-tint-risk);color:var(--mxt-tone-risk)} +.msg.ok{background:var(--mxt-tint-ok);color:var(--mxt-tone-ok)} +.msg.warn{background:var(--mxt-tint-warn);color:var(--mxt-tone-warn)} +/* cards, stats */ +.card{background:var(--mxt-surface);border:1px solid var(--mxt-line);border-radius:var(--mxt-radius-lg);box-shadow:var(--mxt-shadow)} +.card.pad{padding:18px} +.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px} +.phone .stats{gap:8px} +.stat{padding:14px 16px;border-radius:var(--mxt-radius-lg);background:var(--mxt-surface);border:1px solid var(--mxt-line)} +.phone .stat{padding:10px 12px} +.stat .n{font-size:30px;font-weight:600;line-height:1.1;letter-spacing:-.02em} +.phone .stat .n{font-size:24px} +.stat .l{font-size:12px;color:var(--mxt-ink-muted);text-transform:uppercase;letter-spacing:.06em} +.stat.late{background:var(--mxt-tint-warn);border-color:#EAD3A6}.stat.late .n{color:var(--mxt-tone-warn)} +.stat.in{background:var(--mxt-tint-ok);border-color:#BFDCCB}.stat.in .n{color:var(--mxt-tone-ok)} +.sec{display:flex;align-items:center;gap:10px;margin:6px 0 -8px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--mxt-ink-muted);font-weight:600} +.sec .c{background:var(--mxt-tint-neutral);border-radius:99px;padding:0 8px;font-size:12px;color:var(--mxt-ink)} +.sec.late{color:var(--mxt-tone-warn)}.sec.late .c{background:var(--mxt-tint-warn);color:var(--mxt-tone-warn)} +/* pills */ +.pill{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 10px;border-radius:99px;font-size:12px;font-weight:600;white-space:nowrap} +.pill::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor} +.pill.expected{background:var(--mxt-tint-info);color:var(--mxt-tone-info)} +.pill.in{background:var(--mxt-tint-ok);color:var(--mxt-tone-ok)} +.pill.out{background:var(--mxt-tint-neutral);color:var(--mxt-ink-muted)} +.pill.cancelled{background:var(--mxt-tint-neutral);color:var(--mxt-ink-faint);text-decoration:line-through} +.pill.noshow{background:var(--mxt-tint-risk);color:var(--mxt-tone-risk)} +.pill.late{background:var(--mxt-tint-warn);color:var(--mxt-tone-warn)} +.pill.svc{background:var(--mxt-tint-ok);color:var(--mxt-tone-ok)} +.pill.oos{background:var(--mxt-tint-risk);color:var(--mxt-tone-risk)} +/* visit rows */ +.rows{display:flex;flex-direction:column;gap:10px} +.vrow{display:grid;grid-template-columns:78px 1fr auto;align-items:center;gap:16px;padding:14px 18px;background:var(--mxt-surface);border:1px solid var(--mxt-line);border-radius:var(--mxt-radius-lg);box-shadow:var(--mxt-shadow)} +.vrow.late{border-left:5px solid var(--mxt-warning);background:#FFFDF8} +.vrow .t{font-size:20px;font-weight:600;font-variant-numeric:tabular-nums} +.vrow .t small{display:block;font-size:11px;font-weight:600;color:var(--mxt-tone-warn);letter-spacing:.04em;text-transform:uppercase} +.vrow .n{font-size:16px;font-weight:600} +.vrow .m{font-size:13px;color:var(--mxt-ink-muted)} +.vrow .acts{display:flex;gap:8px;align-items:center} +.badge-no{font-variant-numeric:tabular-nums;background:var(--mxt-ink);color:#fff;border-radius:6px;padding:2px 9px;font-weight:600;font-size:13px} +.phone .vrow{grid-template-columns:1fr;gap:10px;padding:12px 14px} +.phone .vrow .acts .btn{flex:1} +.hostcard{padding:14px 16px;background:var(--mxt-surface);border:1px solid var(--mxt-line);border-radius:var(--mxt-radius-lg);display:flex;flex-direction:column;gap:6px} +.hostcard .top2{display:flex;justify-content:space-between;align-items:flex-start;gap:10px} +/* table */ +.tbl{width:100%;border-collapse:collapse;background:var(--mxt-surface);border:1px solid var(--mxt-line);border-radius:var(--mxt-radius-lg);overflow:hidden} +.tbl th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--mxt-ink-muted);text-align:left;padding:10px 16px;background:var(--mxt-surface-alt);border-bottom:1px solid var(--mxt-line)} +.tbl td{padding:12px 16px;border-bottom:1px solid var(--mxt-line);height:56px} +.tbl tr:last-child td{border-bottom:0} +/* dialog */ +.scrim{background:rgba(30,42,50,.45);padding:40px 20px;display:flex;justify-content:center} +.phone .scrim{padding:20px 10px} +.dlg{background:var(--mxt-surface);border-radius:var(--mxt-radius-lg);width:460px;max-width:100%;box-shadow:0 12px 40px rgba(0,0,0,.25);padding:22px;display:flex;flex-direction:column;gap:6px} +.dlg h3{margin:0 0 4px;font-size:18px} +.dlg .foot{display:flex;justify-content:flex-end;gap:8px;margin-top:12px} +.phone .dlg .foot .btn{flex:1} +.empty{text-align:center;padding:36px 16px;color:var(--mxt-ink-muted);border:1px dashed #D2CABD;border-radius:var(--mxt-radius-lg);background:var(--mxt-surface-alt)} +.empty b{display:block;color:var(--mxt-ink);font-size:16px;margin-bottom:4px} +/* binding annotation (wireframe apparatus) */ +.wf-contract{font-size:12px;color:var(--mxt-ink-muted);background:#fff;border:1px dashed #C9C1B4;border-radius:8px;padding:8px 12px;max-width:760px} +.wf-contract p{margin:2px 0} +table.bind{border-collapse:collapse;font-size:12px;background:#fff;max-width:1180px} +table.bind th,table.bind td{border:1px solid var(--mxt-line);padding:4px 8px;text-align:left} +table.bind th{background:var(--mxt-surface-alt)} +/* approval change: Check in is the most prominent thing on an expected visitor row */ +.vrow .btn.primary.big{height:48px;min-width:128px;font-size:16px;box-shadow:0 2px 8px rgba(47,93,98,.35)} +.phone .vrow .btn.primary.big{height:52px;font-size:17px} +.vrow .btn.big:not(.primary){height:40px;font-size:14px} diff --git a/tests/wave2/test-bug03-gates.sh b/tests/wave2/test-bug03-gates.sh index fe8646cb..dcc9597e 100755 --- a/tests/wave2/test-bug03-gates.sh +++ b/tests/wave2/test-bug03-gates.sh @@ -361,7 +361,9 @@ case "$V" in *'APPROVED OVER'*|*'Approved over'*) bad "a forward-filed Stage-6 r echo "== T18: build-ready names a multi-line {{PLACEHOLDER}} left in an agent stub (#509) ==" # The real templates: mdl-agent.md carries PROJECT_LOCAL_SOURCES_OF_TRUTH over four lines. The -# per-line grep never saw it, so an untouched stub read "no unfilled placeholders". WARN only. +# per-line grep never saw it, so an untouched stub read "no unfilled placeholders". Counted as a +# FAIL like any other unfilled slot since the joined-lines count (was a WARN while only the perl scan saw it). +# Decision: an unfilled multi-line placeholder fails build-ready (maintainer, 2026-11-13; supersedes the #509 warning). TK="$(cd "$(dirname "$GATE")/.." && pwd)" P="$(mkproj t18)"; mkdir -p "$P/.claude/agents" for a in ba-agent architect-agent mdl-agent gate-agent test-agent review-agent; do @@ -369,11 +371,10 @@ for a in ba-agent architect-agent mdl-agent gate-agent test-agent review-agent; sed -E 's/\{\{[A-Z][A-Z0-9_]*[^}]*\}\}/filled/g' "$TK/agents/$a.md" > "$P/.claude/agents/$a.md" done OUT="$("$GATE" --no-html "$P" build-ready 2>&1)" -case "$OUT" in *'multi-line placeholder(s) still unfilled: {{PROJECT_LOCAL_SOURCES_OF_TRUTH'*) ok "names the multi-line placeholder" ;; - *) bad "multi-line placeholder not named: $(printf '%s' "$OUT" | grep -i placeholder)" ;; esac +case "$OUT" in *'still have unfilled placeholders: {{PROJECT_LOCAL_SOURCES_OF_TRUTH'*) ok "names the multi-line placeholder as a FAIL" ;; + *) bad "multi-line placeholder not named as unfilled: $(printf '%s' "$OUT" | grep -i placeholder)" ;; esac case "$OUT" in *'no unfilled placeholders'*) bad "still claims no unfilled placeholders" ;; *) ok "does not claim the stubs are complete" ;; esac -case "$OUT" in *'still have unfilled placeholders'*) bad "the multi-line case FAILs (WARN expected): $(printf '%s' "$OUT" | grep -i placeholder)" ;; *) ok "a multi-line placeholder is a WARN, not a FAIL" ;; esac -# Filled: the warning is gone. +# Filled: the failure is gone. perl -0777 -pi -e 's/\{\{PROJECT_LOCAL_SOURCES_OF_TRUTH.*?\}\}/filled/s' "$P/.claude/agents/mdl-agent.md" OUT="$("$GATE" --no-html "$P" build-ready 2>&1)" case "$OUT" in *'no unfilled placeholders'*) ok "a filled stub reads clean" ;; *) bad "filled stub not clean: $(printf '%s' "$OUT" | grep -i placeholder)" ;; esac diff --git a/tests/wave2/test-check-stubs-filled.sh b/tests/wave2/test-check-stubs-filled.sh index 15d28039..fd3570d0 100644 --- a/tests/wave2/test-check-stubs-filled.sh +++ b/tests/wave2/test-check-stubs-filled.sh @@ -13,10 +13,15 @@ out="$(bash "$CK" "$T" 2>&1)"; rc=$? [ $rc -eq 3 ] && ok "T1 fresh stub exits 3" || bad "T1 rc=$rc" echo "$out" | grep -q "UNFILLED mdl-agent" && echo "$out" | grep -q "APP_PURPOSE" && ok "T2 names the slots" || bad "T2 $out" echo "$out" | grep -Eq "UNFILLED mdl-agent: [0-9]+ occurrence\(s\) of [0-9]+ slot\(s\) — " && ok "T2b wording counts occurrences vs unique slots" || bad "T2b $out" -# fill every real slot, keep the DOUBLE_BRACE prose +# fill every single-line slot, keep the DOUBLE_BRACE prose; the shipped stub's one slot that wraps +# over four lines (PROJECT_LOCAL_SOURCES_OF_TRUTH) must still be counted (it used to be missed) f="$T/.claude/agents/mdl-agent.md" sed -E '/DOUBLE_BRACE/!s/\{\{[^}]*\}\}/filled/g' "$f" > "$f.n" && mv "$f.n" "$f" out="$(bash "$CK" "$T" 2>&1)"; rc=$? +[ $rc -eq 3 ] && echo "$out" | grep -q "UNFILLED mdl-agent: 1 occurrence(s) of 1 slot(s) — PROJECT_LOCAL_SOURCES_OF_TRUTH" \ + && ok "T6 a slot wrapped over several lines is counted (and the prose '{{' on line 11 does not swallow it)" || bad "T6 rc=$rc $out" +perl -0777 -pi -e 's/\{\{PROJECT_LOCAL_SOURCES_OF_TRUTH.*?\}\}/filled/s' "$f" +out="$(bash "$CK" "$T" 2>&1)"; rc=$? [ $rc -eq 0 ] && echo "$out" | grep -q "mdl-agent: filled" && ok "T3 filled stub exits 0 (DOUBLE_BRACE prose ignored)" || bad "T3 rc=$rc $out" out="$(bash "$CK" "$T" nosuch 2>&1)"; rc=$? [ $rc -eq 0 ] && echo "$out" | grep -q "no stub" && ok "T4 absent stub said out loud, exit 0" || bad "T4 rc=$rc" diff --git a/tests/wave2/test-design-reaches-app.sh b/tests/wave2/test-design-reaches-app.sh index 81a05d92..31ce1a8d 100755 --- a/tests/wave2/test-design-reaches-app.sh +++ b/tests/wave2/test-design-reaches-app.sh @@ -243,5 +243,75 @@ out="$(run "$d")" hasnt "a descendant selector is not the root theme block" "$out" "tokens valued 9 of 9" has "the descendant block counts as built absent" "$out" "carry the design's value" +# ── 12. CSS layout must not change the reading (#670) ────────────────────────────────────── +# Golden input is REAL (see CAPTURE.md, "#670 captures"): real-ds.css is a design stylesheet with +# several tokens per line, real-built-excerpt.css is verbatim line ranges of a compiled theme +# (bare :root with the design's tokens, a multi-line selector list `.card, ...`, two rules +# inside @media, and `.card-body`). The old readers saw 10 of the design's 38 tokens. +real() { # $1 name $2 design $3 built [$4 "two" for the app/ layout] + d="$TMP/$1"; rm -rf "$d"; mkdir -p "$d/design" "$d/deployment/web" "$d/theme/web" + cp "$2" "$d/design/ds.css"; cp "$3" "$d/deployment/web/theme.compiled.css" + cp "$FIX/real-custom-variables.scss" "$d/theme/web/custom-variables.scss" + if [ "${4:-}" = "two" ]; then + mkdir -p "$d/app"; mv "$d/theme" "$d/deployment" "$d/app/"; : > "$d/app/App.mpr" + fi + printf '%s' "$d" +} +summ() { printf '%s\n' "$1" | grep -E '^(knobs bound|tokens arrived|tokens valued|classes arrived)'; } +tr '\n' ' ' < "$FIX/real-ds.css" > "$TMP/min-ds.css" +tr '\n' ' ' < "$FIX/real-built-excerpt.css" > "$TMP/min-built.css" + +d="$(real realml "$FIX/real-ds.css" "$FIX/real-built-excerpt.css")" +base="$(summ "$(run "$d")")" +has "real sheets: every design token is counted, several per line" "$base" "tokens arrived 32 of 38" +has "real sheets: values compared with the \`}\` gone" "$base" "tokens valued 30 of 38" +has "real sheets: knobs bound through var() are counted" "$base" "knobs bound 31 of 46" +has "real sheets: whole-class count" "$base" "classes arrived 1 of 66" + +# The same two sheets with every newline removed (one line each, comments left in) must read +# identically: nothing in the numbers may depend on how the CSS is laid out. +d="$(real realmin "$TMP/min-ds.css" "$TMP/min-built.css")" +check "one-line design + one-line built read as the multi-line pair" "$(summ "$(run "$d")")" "$base" +check "one-line pair exits like the multi-line pair" "$(code "$d")" "$(code "$(real realml2 "$FIX/real-ds.css" "$FIX/real-built-excerpt.css")")" + +# Both layouts: the same real pair with the app under app/ (.mpr present). +d="$(real realtwo "$FIX/real-ds.css" "$FIX/real-built-excerpt.css" two)" +check "two-tree reads the real pair like the single tree" "$(summ "$(run "$d")")" "$base" +has "two-tree names the built sheet under app/" "$(run "$d")" "built stylesheet app/deployment/web/theme.compiled.css" + +# Small synthetic twins for the individual faults; the real pair above is the golden input. +mini() { # $1 name $2 design css $3 built css + d="$(project "$1" "" true)"; printf '%s' "$2" > "$d/design/ds.css"; printf '%s' "$3" > "$d/deployment/web/theme.compiled.css"; printf '%s' "$d" +} +# fault 1: a one-line :root; the value must not keep its `}`; a later rule's --leak is no token +d="$(mini oneline ':root{--a:1px;--b:2px;--c:3px}.x{--leak:9px}' ':root{--a:1px;--b:2px;--c:3px}')" +out="$(run "$d")" +has "one-line :root declares 3 tokens, not 1" "$out" "tokens arrived 3 of 3" +has "values compare without a trailing brace" "$out" "tokens valued 3 of 3" +# fault 2: minified built sheet still has its bare :root +d="$(mini minbuilt ':root{--brand-primary:#111111}' ':root{--brand-primary:#111111}.x{color:red}')" +has "minified built sheet: bare :root is found, knob bound by literal" "$(run "$d")" "knobs bound 1 of 4" +# fault 3: .card is not arrived because .card-header is +d="$(mini wholeclass '.card{a:b}.hdr{a:b}' '.card-header{a:b}.hdr{a:b}')" +out="$(run "$d")" +has ".card is NOT counted by .card-header" "$out" "classes arrived 1 of 2" +has "names the missing class" "$out" "Absent: .card" +d="$(mini wholeclass2 '.card{a:b}.hdr{a:b}' '.card, .x > .y{a:b}.hdr:hover{a:b}')" +has "a class inside a selector list / pseudo still counts" "$(run "$d")" "classes arrived 2 of 2" +# fault 4: multi-line selector list, rule inside @media +d="$(mini media '@media (min-width: 1px) { + .m1 { a: b } +} +.ml-a, +.ml-b { a: b }' '.m1{a:b}.ml-a{a:b}.ml-b{a:b}')" +has "rules in @media and multi-line selector lists are read" "$(run "$d")" "classes arrived 3 of 3" +# fault 5: var(--x, fallback) uses --x +d="$(mini fallback ':root{--t1:#abcdef}' ':root{--brand-primary:var(--t1, #fff);--font-size-default:var( --nope , 1px)}')" +has "var(--t1, fallback) counts as bound to --t1" "$(run "$d")" "knobs bound 1 of 4" +# a model class: whole-class match against the built sheet +d="$(mini modelcls '.a{a:b}' '.a{a:b}.card-header{a:b}')" +mkdir -p "$d/mdlsource"; printf "Class: 'card'\n" > "$d/mdlsource/p.mdl" +has "model class .card is not defined by .card-header" "$(run "$d")" "1 of 1 classes the MODEL asks for are not defined" + printf '\n%s passed, %s failed\n' "$PASS" "$FAIL" [ "$FAIL" -eq 0 ] || exit 1 diff --git a/tests/wave2/test-exec-result-line.sh b/tests/wave2/test-exec-result-line.sh new file mode 100644 index 00000000..fa655cca --- /dev/null +++ b/tests/wave2/test-exec-result-line.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +# test-exec-result-line.sh - exec.sh prints one fixed MXTK-RESULT line on every path that decided +# a verdict, never as the last line of output, and without changing any exit code. +# +# usage: bash tests/wave2/test-exec-result-line.sh [path-to-exec.sh] +# +# A clean apply -> verdict=applied applied=yes verified=yes lint_new=0 exit=0 +# B lint rise (exit 4) -> verdict=applied-lint ... lint_new=1 exit=4, last line unchanged +# B2 the same under `Lint gate: hard` -> exit=1 +# C bundle-only failure -> verdict=applied-unverified verified=no exit=5, last line unchanged +# D gate finds a new error -> verdict=restored applied=no exit=1 +# E uncommitted model -> verdict=refused applied=no verified=skipped exit=1 +# F usage error (exit 2) -> no RESULT line (no verdict was reached) +# G mxcli exec exit 3, gate passes -> verdict=partial applied=partial exit=1 +# H gate error + broken restore -> verdict=failed applied=unknown exit=1 +# I exec exit 3 + gate error + broken restore -> verdict=failed applied=unknown (same as H); I2 restore ok -> restored +# J-L --patch: clean applied; script fails half-way -> restored (K) / failed when the restore is broken (K2); unverified gate -> restored (L) +# M lint_new words: skipped (SKIP_LINT, lint not installed), blind (lint-gate fault); B: unknown (rise not itemised) +# Every case: exactly one RESULT line, and it is not the last line. +# +# Synthetic only: throwaway git repo, stub mxcli, stub mxbuild, stub java. Nothing real runs. +set -uo pipefail + +TOOLKIT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SUBJECT="${1:-$TOOLKIT/project-bin/exec.sh}" +case "$SUBJECT" in /*) ;; *) SUBJECT="$PWD/$SUBJECT" ;; esac +WORK="$(cd "$(mktemp -d "${TMPDIR:-/tmp}/exec-result.XXXXXX")" && pwd -P)" || exit 2 + +trap 'rm -rf "$WORK"' EXIT +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); echo " ok - $1"; } +bad() { FAIL=$((FAIL+1)); echo " FAIL - $1${2:+: $2}"; } +echo "== subject: $SUBJECT" + +P="$WORK/proj" +mkdir -p "$P/bin" "$P/mprcontents" "$P/mdlsource" "$WORK/jdk/bin" +printf 'not a real model\n' > "$P/Fixture.mpr" +printf 'bson\n' > "$P/mprcontents/a.mxunit" +printf 'CREATE MODULE "Nope";\n' > "$P/mdlsource/test.mdl" +cp "$TOOLKIT/project-bin/_common.sh" "$TOOLKIT/project-bin/restore-mpr.sh" "$P/bin/" +cp "$SUBJECT" "$P/bin/exec.sh" +cat > "$P/bin/snapshot-mpr.sh" <<'SNAP' +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")/.." +D=".mpr-snapshots/$(date +%Y%m%d-%H%M%S)-$$-$RANDOM" +mkdir -p "$D"; cp Fixture.mpr "$D/"; cp -r mprcontents "$D/" +echo "Snapshot saved: $D" +SNAP +# The write: `mxcli exec` appends to the model, as a real script would. +cat > "$P/mxcli" <<'MXCLI' +#!/usr/bin/env bash +D="$(cd "$(dirname "$0")" && pwd)" +case "${1:-}" in + check) exit 0 ;; + run) sleep 120; exit 0 ;; + exec) echo "written by the script" >> "$D/Fixture.mpr"; echo "x" >> "$D/mprcontents/a.mxunit"; echo "fake mxcli exec"; exit "${MXCLI_EXEC_RC:-0}" ;; +esac +exit 0 +MXCLI +chmod +x "$P/bin/"*.sh "$P/mxcli" +# mxbuild: MXB_MODES per call number, comma list: ok | infra (exit 1, no CE code, no errors file) | err (a CE0001 error list) +# MXB_WATCH_PID: record that process's state (ps stat) at the start of every call +# MXB_OTHER_PIDS: the same for more pids, as ` ` lines in MXB_OTHERS +# hang (mode): record the pid in MXB_HANGPID and sleep, so the test can SIGKILL exec.sh mid-gate +cat > "$WORK/mxbuild" <<'MXB' +#!/usr/bin/env bash +n=$(( $(cat "$MXB_N" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$MXB_N" +mode="$(printf '%s\n' "$MXB_MODES" | tr ',' '\n' | sed -n "${n}p")"; mode="${mode:-ok}" +[ -n "${MXB_WATCH_PID:-}" ] && ps -o stat= -p "$MXB_WATCH_PID" >> "$MXB_STATES" +for q in ${MXB_OTHER_PIDS:-}; do echo "$q $(ps -o stat= -p "$q")" >> "$MXB_OTHERS"; done +err=""; for a in "$@"; do case "$a" in --write-errors=*) err="${a#--write-errors=}" ;; esac; done +case "$mode" in + ok) [ -n "$err" ] && printf '{"problems":[]}' > "$err"; exit 0 ;; + hang) echo $$ > "$MXB_HANGPID"; exec sleep 60 ;; + infra) echo "Directory not empty : deployment/web" >&2; exit 1 ;; + err) [ -n "$err" ] && printf '{"problems":[{"severity":"Error","code":"CE0001","message":"boom","element":{"document":"M.X"}}]}' > "$err"; exit 1 ;; + # #448 field shapes (mxbuild exit 3, problems[] empty): a deploy-stage message, in the dict and + # the plain-string errors[] form, and the deterministic version mismatch (never retried) + bundle) printf '{"errors":[{"message":"Compilation of the app bundle failed.","details":""}],"problems":[]}' > "$err"; echo "Compilation of the app bundle failed."; exit 3 ;; + bundle2) printf '{"problems":[],"errors":["An error occurred while compiling Theme files"]}' > "$err"; exit 3 ;; + version) printf '{"errors":[{"message":"The MPR file located at /x/App.mpr could not be opened: Project version '"'"'11.12.2'"'"' does not exactly match MxBuild version '"'"'11.14.0'"'"'. Use loose version check option for less strict version checking.","details":""}],"problems":[]}' > "$err"; exit 3 ;; +esac +MXB +printf '#!/usr/bin/env bash\nexit 0\n' > "$WORK/jdk/bin/java" +chmod +x "$WORK/mxbuild" "$WORK/jdk/bin/java" +( cd "$P" && git init -q . && printf '.mpr-snapshots/\ndocs/\n.claude/\n' > .gitignore && git add -A \ + && git -c user.email=t@t -c user.name=t commit -qm fixture ) >/dev/null 2>&1 + +LOG="$P/docs/BUILD-LOG.md" +sum() { cat "$P/Fixture.mpr" "$P/mprcontents/a.mxunit" | cksum; } +BEFORE="$(sum)" # the committed model; run() resets to it +# run