From 487c390285f844d0c950ca741859bd25378f9777 Mon Sep 17 00:00:00 2001 From: saingge <262679532+saingge@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:25:37 +0200 Subject: [PATCH 1/2] new(model-scope): find and put back units a write changed only in XPath whitespace mxcli v0.27.0's `alter entity ... rename attribute` rewrote 135 units in 20 modules of a Mendix 10.24.18 app that never referenced the attribute. The only change in each was the line breaks between bracketed XPath conditions; `mxcli diff-local` showed nothing (mendixlabs/mxcli#1596). project-bin/model-scope.py: `report` sorts changed units into real and noise, `restore` checks the noise out of HEAD and updates its ContentsHash rows through mxpatch.py, `check --staged` exits 1 on staged noise (MODEL_SCOPE_OK=1 overrides once). Not wired into exec.sh or the hook yet; that is the next PR. Fixture tests/wave2/test-model-scope.sh, including a golden before/after capture of a real v0.27.0 rename on a public model (fixtures/model-scope/golden-v0.27.0/, CAPTURE.md). --- CHANGELOG.md | 1 + bin/lib/install-manifest.sh | 4 +- project-bin/model-scope.py | 410 ++++++++++++++++++ .../model-scope/golden-v0.27.0/CAPTURE.md | 54 +++ .../golden-v0.27.0/add-linebreaks.py | 13 + .../golden-v0.27.0/after/TestApp.mpr.sql | 54 +++ ...4776610-13b2-48cb-a265-d5e984d63129.mxunit | Bin 0 -> 5752 bytes ...2e900a8-6b87-4916-ae7b-49fed7f0a939.mxunit | Bin 0 -> 2595 bytes ...e321a44-1103-4ee9-875b-6d521bdd3178.mxunit | Bin 0 -> 2587 bytes .../golden-v0.27.0/before/TestApp.mpr.sql | 54 +++ ...6747dc6-b6b7-42ae-aabf-255dca2aeeaf.mxunit | Bin 0 -> 236 bytes ...4776610-13b2-48cb-a265-d5e984d63129.mxunit | Bin 0 -> 5751 bytes ...2e900a8-6b87-4916-ae7b-49fed7f0a939.mxunit | Bin 0 -> 2596 bytes ...e321a44-1103-4ee9-875b-6d521bdd3178.mxunit | Bin 0 -> 2588 bytes ...83ca969-ff50-4e5b-b61d-b1ffe55e03d9.mxunit | Bin 0 -> 228 bytes .../golden-v0.27.0/capture-base.mdl | 10 + .../golden-v0.27.0/capture-rename.mdl | 2 + .../fix-hashes-after-restore-v0.27.0.out | 3 + .../golden-v0.27.0/rename-v0.27.0.exit | 1 + .../golden-v0.27.0/rename-v0.27.0.out | 4 + .../golden-v0.27.0/restore-v0.27.0.out | 4 + tests/wave2/fixtures/model-scope/mkmodel.py | 228 ++++++++++ tests/wave2/test-model-scope.sh | 179 ++++++++ 23 files changed, 1020 insertions(+), 1 deletion(-) create mode 100755 project-bin/model-scope.py create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/CAPTURE.md create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/add-linebreaks.py create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/after/TestApp.mpr.sql create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/after/mprcontents/84/77/84776610-13b2-48cb-a265-d5e984d63129.mxunit create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/after/mprcontents/c2/e9/c2e900a8-6b87-4916-ae7b-49fed7f0a939.mxunit create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/after/mprcontents/ce/32/ce321a44-1103-4ee9-875b-6d521bdd3178.mxunit create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/before/TestApp.mpr.sql create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/16/74/16747dc6-b6b7-42ae-aabf-255dca2aeeaf.mxunit create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/84/77/84776610-13b2-48cb-a265-d5e984d63129.mxunit create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/c2/e9/c2e900a8-6b87-4916-ae7b-49fed7f0a939.mxunit create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/ce/32/ce321a44-1103-4ee9-875b-6d521bdd3178.mxunit create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/f8/3c/f83ca969-ff50-4e5b-b61d-b1ffe55e03d9.mxunit create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/capture-base.mdl create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/capture-rename.mdl create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/fix-hashes-after-restore-v0.27.0.out create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/rename-v0.27.0.exit create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/rename-v0.27.0.out create mode 100644 tests/wave2/fixtures/model-scope/golden-v0.27.0/restore-v0.27.0.out create mode 100755 tests/wave2/fixtures/model-scope/mkmodel.py create mode 100755 tests/wave2/test-model-scope.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ee82ab78..9cb2174e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- new(model-scope): **a write's whitespace-only units can now be found and put back.** mxcli v0.27.0's `alter entity … rename attribute` rewrote 135 units in 20 modules of a Mendix 10.24.18 app that never referenced the attribute; the only change in each was the line breaks between bracketed XPath conditions, and `mxcli diff-local` showed nothing (mendixlabs/mxcli#1596). New `project-bin/model-scope.py`: `report` sorts each changed unit into real or noise (every differing field is an XPath constraint that is equal once whitespace outside string literals is removed) and prints the real changes per module; `restore` checks the noise units out of `HEAD` and updates their ContentsHash rows through `mxpatch.py`; `check --staged` exits 1 when noise is staged (`MODEL_SCOPE_OK=1` lets one commit through). Not wired into `exec.sh` or the pre-commit hook yet. Fixture `tests/wave2/test-model-scope.sh`, with a golden before/after capture of a real v0.27.0 rename on a public model (`fixtures/model-scope/golden-v0.27.0/`) — saingge - fix(progress-report, build-plan-status): **HARNESS rows are no longer counted as built rows, and a module's `verify-module.sh` row shows once, as that module's sign-off status on its phase card, instead of as a row.** A HARNESS step is a module's sign-off, not a build, so it is out of every "x of y built" count: the phase cards, the plan rollup and the Stage 5 card in `progress-report.html`, and the lede of `build-plan-status.sh --html` ("N build rows", plus a "Not counted: N HARNESS rows" note). The verify row's evidence is read exactly as #501 reads it (`harness_verify`'s rule, now `verify_evidence` + `module_status` in `progress-report.py`; nothing new measured, no extra script run, no mxcli or mxbuild call) and shown as one line per module: `Verified ✓ (LOOK n/n)`, `Verify owed - LOOK + CONFIRM`, `Verify stale (N builds since)` (or `(a script was marked done- after it)`), `Verify not run`, `built? no verify summary`, `Verify not clean ()` for a summary with a FAULT, FINDING or SKIPPED row, or `Verify clean , LOOK n/n - held: State says ` when State is blocked or pending a person (held is not signed off, as #501 never promoted it); a State cell that disagrees is flagged under it, the rollup says how many modules are verified, and the Stage 5 card, KPI and owed text add ", k of n modules signed off". The Phase state badge is read from the build rows only (a phase of HARNESS rows alone says "sign-off only" and its card "Sign-off phase: no build rows"). A module with no verify row shows no status. Other HARNESS rows (a coherence pass, a harness run) stay in the row list, marked "(not counted)". `build-plan.md`, `build-plan.json` and the text output are unchanged, so agents still see every HARNESS step. **Behaviour change to check in your project:** the progress report's built counts drop the HARNESS rows (a plan of 10 rows with 2 HARNESS reads "x of 8"), and the verify rows leave the row tables. Fixture: `test-progress-harness-verify.sh` (H1-H10, N1-N6, both layouts). — MendixMau (decision 2026-10-21) - fix(exec): **a script restated from a stale DESCRIBE now WARNs instead of silently reverting an earlier exec** (#261). A script drafted from a DESCRIBE taken before an earlier script of the batch ran put four old messages back, with check, mxbuild and lint green. New `project-bin/describe-baseline.sh stamp` writes `-- baseline: sha256=` (plus `-- baseline-mxcli: `) from a DESCRIBE at draft time; `exec.sh` runs `verify` only for a script that carries the header, so other scripts cost one grep and no mxcli call. It reports DRIFT with the first lines of the diff, STALEHEADER, and RESTAMP after an mxcli upgrade (DESCRIBE text may change, so that is not called drift). A re-run by the script's own last recorded write (the #269 ledger) and a fresh model are not drift. Nothing is refused and the exit codes do not change. Preflight STOP 21 gains one clause (`-- supersedes:`, `stamp`, exec.sh only warns) and its trigger names `create or modify` (+24 words). Not-found DESCRIBE and `--version` output are captured from mxcli v0.26.0. Lands after the #269 PR; with it, supersedes draft #311. **Behaviour change to check in your project:** `exec.sh` output can carry `WARN: stale draft: …` lines, and the BUILD-LOG detail cell `WARN stale baseline: `; `MXTK_RESTATE_GUARD=off` skips the check. — AlvinnYeo (#261) - fix(exec): **re-running an older `mdlsource/` script over a newer owner's live work now WARNs instead of passing silently** (#269). Three field apps had 99/579, 130/357 and 38/159 documents created by more than one script, and a replay of an older one reverted the newer with exit 0 and every gate green. New `project-bin/script-owners.sh` maps each microflow, nanoflow, page and snippet to the scripts that create or restate it, in run order (digit runs compared as numbers, so `2-core/` runs before `11-fixes/`; `done-` ignored), and reads `-- supersedes:` / `-- superseded-by:` headers. `exec.sh` records every kept write in a machine-local last-writer ledger (`.claude/loop/script-owners/`, self-gitignored, compacted per write); `restore-mpr.sh` marks it on a manual restore. A replay WARNs only when the newer owner's write is live: ledger first, else one DESCRIBE per document, at most 5 per exec (the rest print `CONFLICT (not checked)`). A new owner without a supersedes header gets one `NOTE:` line on its first run, kept out of BUILD-LOG. Nothing is refused and the exit codes do not change. With no co-owner, `exec.sh` runs one grep over `mdlsource/` and no mxcli call. `gate-check.sh` prints the co-owned count when it is above 0. Part of the rebuilt draft #311 (with the #261 PR). **Behaviour change to check in your project:** `exec.sh` output can carry `WARN: restatement: …` and `NOTE: new owner of …` lines; the BUILD-LOG detail cell can carry `WARN reverts: `. Set `MXTK_RESTATE_GUARD=off` to skip the check. — MendixMau (#269) diff --git a/bin/lib/install-manifest.sh b/bin/lib/install-manifest.sh index f674fcba..ba30cdd1 100644 --- a/bin/lib/install-manifest.sh +++ b/bin/lib/install-manifest.sh @@ -123,7 +123,9 @@ MXTK_AGENTS_STAGE_BUILD="mdl-agent.md gate-agent.md test-agent.md review-agent.m # write (co-owned documents) and records each kept write in its ledger; restore-mpr.sh marks it. # describe-baseline.sh joined the same day (#261): exec.sh runs its verify for a script with a # `-- baseline:` header, and it reads script-owners.sh's ledger as ITS sibling. -MXTK_PROJECT_BIN="process-map.js process-map.template.html app-restart.sh test-scratch.sh script-owners.sh describe-baseline.sh assemble-prototype.js prototype-route.js check-prototype-links.js wf-add-path-terminators.py wf-set-call-captions.py mxunit_bson.py mxpatch.py _common.sh app-facts.sh _claims.sh snapshot-mpr.sh restore-mpr.sh exec.sh affected-tests.sh look-ledger.sh save-sp.sh restart-sp.sh check-sp-health.sh verify-module.sh test-stack-up.sh fixture-manifest.sh check-root-clean.sh lint-gate.sh lint-trend.sh metrics-ledger.sh metrics-trend.sh close-task.sh conformance-check.sh coverage-preflight.sh graph-sweep.sh review-module.sh coherence-cadence.sh build-plan-status.sh progress-report.py done-drift-check.sh page-scope.sh render-improvement-register.sh check-design-portability.sh check-design-reaches-app.sh check-page-shell.sh page-fidelity.js model-stamp.sh verify-model.sh install-project-hooks.sh mx-metadata-note.py session-check.sh constants-audit.sh context-pack.sh check-staged-secrets.sh" +# model-scope.py joined on 2026-10-21: it imports mxpatch.py (and through it mxunit_bson.py) from +# its own directory. It puts back units a write changed only in XPath whitespace (mxcli#1596). +MXTK_PROJECT_BIN="process-map.js process-map.template.html app-restart.sh test-scratch.sh script-owners.sh describe-baseline.sh assemble-prototype.js prototype-route.js check-prototype-links.js wf-add-path-terminators.py wf-set-call-captions.py model-scope.py mxunit_bson.py mxpatch.py _common.sh app-facts.sh _claims.sh snapshot-mpr.sh restore-mpr.sh exec.sh affected-tests.sh look-ledger.sh save-sp.sh restart-sp.sh check-sp-health.sh verify-module.sh test-stack-up.sh fixture-manifest.sh check-root-clean.sh lint-gate.sh lint-trend.sh metrics-ledger.sh metrics-trend.sh close-task.sh conformance-check.sh coverage-preflight.sh graph-sweep.sh review-module.sh coherence-cadence.sh build-plan-status.sh progress-report.py done-drift-check.sh page-scope.sh render-improvement-register.sh check-design-portability.sh check-design-reaches-app.sh check-page-shell.sh page-fidelity.js model-stamp.sh verify-model.sh install-project-hooks.sh mx-metadata-note.py session-check.sh constants-audit.sh context-pack.sh check-staged-secrets.sh" # Files in project-bin/ that are deliberately NOT installed into projects. The reverse check # below flags anything named by NEITHER list, so a new file in project-bin/ has to be either diff --git a/project-bin/model-scope.py b/project-bin/model-scope.py new file mode 100755 index 00000000..77cd82f4 --- /dev/null +++ b/project-bin/model-scope.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 +"""model-scope.py -- find, and put back, model units whose bytes changed but whose meaning did not. + + python3 -I bin/model-scope.py report [--against REF] [--mpr FILE] [-v] + python3 -I bin/model-scope.py restore [--against REF] [--mpr FILE] + python3 -I bin/model-scope.py check --staged [--project DIR] + +WHY. mxcli v0.27.0 on a Mendix 10.24.18 app (MPR v2): one `alter entity ... rename attribute` +rewrote 135 units in 20 modules that never referenced the attribute. In each one the only change +was whitespace in XPath constraint strings: the line breaks between bracketed conditions were +removed (mendixlabs/mxcli#1596). `mxcli diff-local` renders MDL, which was identical, so it showed +nothing; the reviewer found the units in the branch diff. Captured on a public model +(tests/wave2/fixtures/model-scope/golden-v0.27.0/): renaming an unused attribute turned +"[Label = 'A']\\n[Flag != 'B']" into "[Label = 'A'][Flag != 'B']" in a microflow of another module. + +NOISE vs REAL. A changed unit is noise when both versions decode to the same fields in the same +order, and every field that differs is an XPath constraint whose old and new strings are equal +once whitespace OUTSIDE string literals is removed. Anything else is real: another field, a +changed structure, whitespace inside a quoted literal, a unit that cannot be decoded, an added or +deleted unit. A newly proven kind of noise goes into same_meaning(), nowhere else. + +MODES + report Classify every unit file changed in the working tree against REF (default HEAD). + Prints the real changes per module and the noise count. Changes nothing. -v lists + every unit. + restore report, then put each noise unit back from REF (`git checkout REF -- `) and + set its ContentsHash row in the .mpr from the restored file (mxpatch.py's rehash), so + the index Studio Pro reads stays in step. bin/exec.sh runs this after every write. + check --staged: the staged index against HEAD, for the pre-commit hook. Exit 1 when a + noise unit is staged, printing the restore command. MODEL_SCOPE_OK=1 lets that one + commit through with a warning. + +EXIT 0 ok 1 check: noise is staged 2 usage or environment problem (a tool fault, never a verdict) + +MPR v2 only: a v1 .mpr keeps its units inside itself, so there is nothing to scope (exit 0). +Stdlib plus mxpatch.py and mxunit_bson.py from this directory; run it with `python3 -I`. +""" +import argparse +import collections +import os +import sqlite3 +import subprocess +import sys +import tempfile +import uuid + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import mxpatch # noqa: E402 (beside this file) +from mxunit_bson import dec_doc # noqa: E402 + + +def decode(raw): + doc, end = dec_doc(raw, 0) + if end != len(raw): + raise ValueError("trailing bytes after the BSON document") + return doc + + +class ScopeError(Exception): + pass + + +# -------------------------------------------------------------------------------------------- +# Is a changed unit noise? +# -------------------------------------------------------------------------------------------- +def strip_ws_outside_literals(s): + """s without the whitespace that sits outside '...' and "..." literals. A backslash inside a + literal escapes the next character. An unterminated literal runs to the end, so a doubtful + case keeps its whitespace and can only come out REAL, never noise.""" + out, quote, i = [], None, 0 + while i < len(s): + c = s[i] + if quote: + out.append(c) + if c == "\\" and i + 1 < len(s): + i += 1 + out.append(s[i]) + elif c == quote: + quote = None + elif c in "'\"": + quote = c + out.append(c) + elif not c.isspace(): + out.append(c) + i += 1 + return "".join(out) + + +def same_meaning(key, old, new): + """True when this one differing leaf field means the same in both versions. Microflows store + the key as XpathConstraint, page data sources as XPathConstraint.""" + return (key.lower() == "xpathconstraint" and isinstance(old, str) and isinstance(new, str) + and strip_ws_outside_literals(old) == strip_ws_outside_literals(new)) + + +def _leaves(doc, path=()): + """[(path, type, value)] for every field, depth first; documents and arrays carry no value.""" + out = [] + for t, k, v in doc: + if t in (0x03, 0x04): + out.append((path + (k,), t, None)) + out.extend(_leaves(v, path + (k,))) + else: + out.append((path + (k,), t, (v.sub, bytes(v.data)) if t == 0x05 else v)) + return out + + +def classify(old_raw, new_raw): + """("noise" | "real", reason) for a unit present on both sides whose bytes differ.""" + try: + old, new = _leaves(decode(old_raw)), _leaves(decode(new_raw)) + except Exception as e: # an unknown BSON type, a truncated file, ... + return "real", "cannot decode (%s)" % e + if [(p, t) for p, t, _ in old] != [(p, t) for p, t, _ in new]: + return "real", "structure changed" + diffs = [(p, a, b) for (p, _, a), (_, _, b) in zip(old, new) if a != b] + for p, a, b in diffs: + if not same_meaning(p[-1], a, b): + return "real", "field changed: /" + "/".join(p) + if not diffs: + return "real", "bytes differ, decoded fields equal" + return "noise", "%d XPath constraint(s), whitespace only" % len(diffs) + + +# -------------------------------------------------------------------------------------------- +# git +# -------------------------------------------------------------------------------------------- +def pin_git_env(): + """A hook may export GIT_DIR / GIT_INDEX_FILE as relative paths. Make them absolute before + any chdir, and pin the work tree, so every later git call sees the git that called us.""" + for k in ("GIT_INDEX_FILE", "GIT_DIR", "GIT_WORK_TREE"): + if os.environ.get(k): + os.environ[k] = os.path.abspath(os.environ[k]) + if os.environ.get("GIT_DIR") and not os.environ.get("GIT_WORK_TREE"): + os.environ["GIT_WORK_TREE"] = git(".", "rev-parse", "--show-toplevel").strip() + + +def git(cwd, *args): + p = subprocess.run(("git",) + args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if p.returncode != 0: + raise ScopeError("git %s failed: %s" % (" ".join(args[:2]), p.stderr.decode("utf-8", "replace").strip())) + return p.stdout.decode("utf-8", "replace") + + +def blobs(top, specs): + """{spec: bytes or None} for `REV:path` / `:path` specs, in one `git cat-file --batch`.""" + if not specs: + return {} + p = subprocess.run(["git", "cat-file", "--batch"], cwd=top, input="".join(s + "\n" for s in specs).encode(), + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, i, data = {}, 0, p.stdout + for s in specs: + j = data.index(b"\n", i) + head = data[i:j].split() + i = j + 1 + if len(head) == 3 and head[1] == b"blob": + n = int(head[2]) + out[s], i = data[i:i + n], i + n + 1 + else: + out[s] = None + return out + + +def find_mpr(explicit): + for p in (explicit, os.environ.get("MPR_FILE")): + if p: + return os.path.abspath(p) + for d in (".", "app"): + found = sorted(f for f in os.listdir(d) if f.lower().endswith(".mpr")) if os.path.isdir(d) else [] + if len(found) > 1: + raise ScopeError("more than one .mpr in %s; name one with --mpr" % os.path.abspath(d)) + if found: + return os.path.abspath(os.path.join(d, found[0])) + return None + + +# -------------------------------------------------------------------------------------------- +# The changed units +# -------------------------------------------------------------------------------------------- +Item = collections.namedtuple("Item", "status rel verdict reason module") + + +class Scope(object): + """The changed units of one model: working tree (or index, staged=True) against ref.""" + + def __init__(self, mpr, ref, staged): + self.mpr, self.ref, self.staged = os.path.realpath(mpr), ref, staged + model_dir = os.path.dirname(self.mpr) + self.top = os.path.realpath(git(model_dir, "rev-parse", "--show-toplevel").strip()) + prefix = os.path.relpath(model_dir, self.top).replace(os.sep, "/") + self.prefix = "" if prefix == "." else prefix + "/" + if subprocess.run(["git", "rev-parse", "-q", "--verify", ref + "^{commit}"], cwd=self.top, + stdout=subprocess.DEVNULL).returncode != 0: + raise ScopeError("%s is not a commit in this repository" % ref) + + def changed(self): + """[(status, repo-relative path)] of unit files that differ from the ref.""" + spec = ":(literal)" + self.prefix + "mprcontents" + args = ["diff", "--cached"] if self.staged else ["diff"] + tok = git(self.top, *(args + ["--name-status", "-z", "--no-renames", self.ref, "--", spec])).split("\0") + out = [(tok[i][:1], tok[i + 1]) for i in range(0, len(tok) - 1, 2)] + if not self.staged: # an untracked unit file is an addition too + out += [("A", f) for f in git(self.top, "ls-files", "-z", "--others", "--exclude-standard", + "--", spec).split("\0") if f] + return [(s, f) for s, f in out if f.endswith(".mxunit")] + + def new_bytes(self, rels): + if self.staged: + return blobs(self.top, [":" + r for r in rels]) + out = {} + for r in rels: + p = os.path.join(self.top, r) + out[":" + r] = open(p, "rb").read() if os.path.isfile(p) else None + return out + + def modules(self): + """{unit id: (container id, containment)} from the .mpr's Unit table, new side first; the + ref's table adds the units this change deleted.""" + name = self.prefix + os.path.basename(self.mpr) + sides = [blobs(self.top, [":" + name])[":" + name] if self.staged else self.mpr, + blobs(self.top, ["%s:%s" % (self.ref, name)])["%s:%s" % (self.ref, name)]] + parents = {} + for side in sides: + path = side + if isinstance(side, bytes): + fd, path = tempfile.mkstemp(suffix=".mpr") + with os.fdopen(fd, "wb") as fh: + fh.write(side) + try: + if path: + db = sqlite3.connect("file:%s?mode=ro" % path, uri=True, timeout=5) + for u, c, n in db.execute("SELECT UnitID, ContainerID, ContainmentName FROM Unit"): + parents.setdefault(bytes(u), (bytes(c) if c is not None else None, n)) + db.close() + except sqlite3.Error: + pass + finally: + if isinstance(side, bytes): + os.remove(path) + return parents + + def module_of(self, uid, parents, names): + cur, seen = uid, set() + while cur is not None and cur not in seen: + seen.add(cur) + parent, containment = parents.get(cur, (None, None)) + if containment == "Modules": + if cur not in names: + rel = self.unit_rel(cur) + raw = self.new_bytes([rel])[":" + rel] or blobs(self.top, ["%s:%s" % (self.ref, rel)]).popitem()[1] + try: + names[cur] = mxpatch.get(decode(raw), "Name") + except Exception: + names[cur] = None + return names[cur] + cur = parent + return None + + def unit_rel(self, uid): + u = str(uuid.UUID(bytes_le=uid)) + return "%smprcontents/%s/%s/%s.mxunit" % (self.prefix, u[:2], u[2:4], u) + + def scan(self): + """[Item] for every changed unit file.""" + changed = self.changed() + old = blobs(self.top, ["%s:%s" % (self.ref, r) for s, r in changed if s == "M"]) + new = self.new_bytes([r for s, r in changed if s == "M"]) + parents, names, items = self.modules(), {}, [] + for status, rel in changed: + if status == "M": + a, b = old["%s:%s" % (self.ref, rel)], new[":" + rel] + if a == b: + continue + verdict, reason = classify(a, b) if a is not None and b is not None else ("real", "unreadable") + else: + verdict, reason = "real", {"A": "added", "D": "deleted"}.get(status, "git status " + status) + try: + mod = self.module_of(mxpatch.uid_blob(rel), parents, names) + except Exception: + mod = None + items.append(Item(status, rel, verdict, reason, mod or "(project)")) + return items + + +def summary(items, against): + real = [i for i in items if i.verdict == "real"] + noise = [i for i in items if i.verdict == "noise"] + lines = ["model scope vs %s: %d unit file(s) changed, %d real, %d noise" + % (against, len(items), len(real), len(noise))] + if real: + per = collections.Counter(i.module for i in real) + lines.append(" real changes by module: " + ", ".join( + "%s %d" % (k, n) for k, n in sorted(per.items(), key=lambda kv: (-kv[1], kv[0])))) + if noise: + lines.append(" noise: %d unit(s) in %d module(s), XPath whitespace only" + % (len(noise), len(set(i.module for i in noise)))) + return lines + + +# -------------------------------------------------------------------------------------------- +# Modes +# -------------------------------------------------------------------------------------------- +def report(scope, verbose): + items = scope.scan() + print("\n".join(summary(items, scope.ref))) + if verbose: + for i in sorted(items, key=lambda i: (i.verdict, i.module, i.rel)): + print(" %-5s %s %-20s %s [%s]" % (i.verdict, i.status, i.module, i.rel, i.reason)) + return items + + +def restore(scope): + noise = [i for i in report(scope, False) if i.verdict == "noise"] + if not noise: + return 0 + files = [i.rel for i in noise] + failed = None + try: + for k in range(0, len(files), 100): + git(scope.top, "checkout", scope.ref, "--", *files[k:k + 100]) + except ScopeError as e: + failed = e + finally: + # Every listed file, restored or not: rehash sets each row from the bytes on disk, so a + # checkout that stopped part-way never leaves a restored file with a stale row. + try: + mxpatch.Model(scope.mpr).rehash([os.path.join(scope.top, f) for f in files], + apply=True, log=lambda s: None) + except Exception as e: + raise ScopeError("some of the %d noise unit file(s) may be back from %s, and their " + "ContentsHash rows were not updated (%s). Repair the index: " + "./mxcli fix hashes -p %s --repair" + % (len(files), scope.ref, e, os.path.basename(scope.mpr))) + if failed: + raise ScopeError("restore stopped part-way (%s). The index matches every unit file; " + "units not yet restored are as the write left them. Run restore again." + % failed) + print(" restored %d noise unit(s) from %s, ContentsHash rows updated" % (len(files), scope.ref)) + return 0 + + +def check_staged(scope): + items = scope.scan() + noise = [i for i in items if i.verdict == "noise"] + if not noise: + return 0 + override = os.environ.get("MODEL_SCOPE_OK") == "1" + err = sys.stderr.write + err("\n".join(summary(items, "HEAD (staged)")) + "\n\n") + err(" %s: %d staged unit file(s) changed only in XPath whitespace, not in meaning:\n" + % ("WARNING" if override else "commit refused", len(noise))) + for i in noise[:8]: + err(" %-20s %s\n" % (i.module, i.rel)) + if len(noise) > 8: + err(" ... and %d more\n" % (len(noise) - 8)) + err(" A reviewer would have to open every one of them. From the project root, put them back:\n" + " python3 -I bin/model-scope.py restore\n" + " then stage the model again. To commit them anyway, for this one commit:\n" + " MODEL_SCOPE_OK=1 git commit ...\n" + " (the right choice when you reformatted those XPaths on purpose, e.g. in Studio Pro).\n") + if override: + err(" MODEL_SCOPE_OK=1: committing the noise units anyway.\n") + return 0 + return 1 + + +def main(argv): + ap = argparse.ArgumentParser(prog="model-scope.py", description=__doc__.split("\n\n")[0]) + ap.add_argument("mode", choices=("report", "restore", "check")) + ap.add_argument("--against", default="HEAD", metavar="REF") + ap.add_argument("--mpr", metavar="FILE") + ap.add_argument("--staged", action="store_true", help="check: compare the index with HEAD") + ap.add_argument("--project", metavar="DIR", help="run as if from this directory (the hook passes it)") + ap.add_argument("-v", "--verbose", action="store_true") + args = ap.parse_args(argv) + if args.mode == "check" and not args.staged: + ap.error("check needs --staged") + try: + pin_git_env() + if args.project: + os.chdir(args.project) + mpr = find_mpr(args.mpr) + if mpr is None or not os.path.isfile(mpr): + if args.mode == "check": + return 0 # no model yet: nothing to check + raise ScopeError("no .mpr found (run from the project root, or pass --mpr)") + if not os.path.isdir(os.path.join(os.path.dirname(mpr), "mprcontents")): + print("model scope: %s is MPR v1 (units inside the .mpr): nothing to scope" % os.path.basename(mpr)) + return 0 + if args.mode == "check": + # Nothing to compare to on an unborn branch. + if subprocess.run(["git", "rev-parse", "-q", "--verify", "HEAD"], stdout=subprocess.DEVNULL, + cwd=os.path.dirname(mpr)).returncode != 0: + return 0 + return check_staged(Scope(mpr, "HEAD", staged=True)) + scope = Scope(mpr, args.against, staged=False) + if args.mode == "restore": + return restore(scope) + report(scope, args.verbose) + return 0 + except ScopeError as e: + sys.stderr.write("model-scope.py: %s\n" % e) + return 2 + except Exception as e: # a tool fault must never read as "noise found" (1) or as a pass + sys.stderr.write("model-scope.py: internal error: %s: %s\n" % (type(e).__name__, e)) + return 2 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/CAPTURE.md b/tests/wave2/fixtures/model-scope/golden-v0.27.0/CAPTURE.md new file mode 100644 index 00000000..465c4ce7 --- /dev/null +++ b/tests/wave2/fixtures/model-scope/golden-v0.27.0/CAPTURE.md @@ -0,0 +1,54 @@ +# golden-v0.27.0 — provenance (captured 2026-10-21, macOS arm64) + +A real `alter entity … rename attribute` by mxcli v0.27.0 on a public model, before and after. +Nothing here was hand-written except this file, the two MDL scripts and `add-linebreaks.py` +(they told mxcli and mxpatch what to do). The unit files and `.mpr` dumps are what the tools +wrote. It reproduces mendixlabs/mxcli#1596. + +## Binary + +`mxcli version v0.27.0 (2026-10-15T13:45:04Z)`, the public release binary. + +## Model + +Base: `tests/wave2/fixtures/mxpatch/model/` of this repo (MPR v2, Mendix 11.14.0; its own +provenance is in `fixtures/mxpatch/CAPTURE.txt`), with the `.mpr` rebuilt from its sqlite dump. +On a copy, in a fresh git repository: + +1. `mxcli exec capture-base.mdl -p TestApp.mpr`. It creates module `Other`, the entities + `MyFirstModule.Item` (with an attribute `Unused` that no XPath names) and `Other.Thing`, and + one microflow in each module that retrieves with a two-condition XPath. mxcli stores those + XPaths on one line, `[Label = 'A'][Flag != 'B']` (mendixlabs/mxcli#1542). +2. `python3 -I add-linebreaks.py /project-bin TestApp.mpr`. Through `mxpatch.py`, this + puts `\n` between the bracket groups of both stored `XpathConstraint` values and writes each + unit and its ContentsHash row together. That gives `[Label = 'A']\n[Flag != 'B']`, the + two-line form the Studio Pro-authored XPaths of the field incident had. `mxcli describe microflow + Other.ACT_Thing_Fetch` then shows the condition on two lines, and `mxcli fix hashes` reports + that the index matches every unit file. This state was committed. It is `before/`. +3. `mxcli exec capture-rename.mdl -p TestApp.mpr` renames `MyFirstModule.Item.Unused` to + `Unused2`. Its stdout is `rename-v0.27.0.out` and its exit code is `rename-v0.27.0.exit` + (stderr was empty). It prints "Updated 2 XPath constraint(s) in 2 document(s)". + `git status` then lists `TestApp.mpr` and three unit files. That state is `after/`. + +## Files + +- `before/TestApp.mpr.sql` and `after/TestApp.mpr.sql`: the `.mpr` as a sqlite dump. `*.mpr` + is gitignored here, so the test rebuilds the file with `executescript`. +- `before/mprcontents/`: the three units the rename rewrote, plus the two module units + (`16747dc6-…` MyFirstModule, `f83ca969-…` Other) that the module walk reads. +- `after/mprcontents/`: the same three units as the rename left them. + - `84776610-…` is the MyFirstModule domain model. This change is real: the attribute's + `Name` changed. + - `c2e900a8-…` is `MyFirstModule.ACT_Item_Fetch`. This change is noise: + `[Status = 'Open']\n[Code = 'X']` became `[Status = 'Open'][Code = 'X']`. + - `ce321a44-…` is `Other.ACT_Thing_Fetch`, in a module the script never named. This change is + noise: `[Label = 'A']\n[Flag != 'B']` became `[Label = 'A'][Flag != 'B']`. +- `restore-v0.27.0.out`: `model-scope.py restore` run on a copy of `after/` in that repository. +- `fix-hashes-after-restore-v0.27.0.out`: `mxcli fix hashes -p TestApp.mpr` run right after that + restore, with blank lines dropped. It exited 0, and the output says the index matches every + unit file. `describe microflow Other.ACT_Thing_Fetch` showed the two-line XPath again. + +## Not checked + +The capture did not run `mx check` / mxbuild on the restored model. Mendix 11.14.0 mxbuild was +not available on the capture machine. diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/add-linebreaks.py b/tests/wave2/fixtures/model-scope/golden-v0.27.0/add-linebreaks.py new file mode 100644 index 00000000..df6cec9d --- /dev/null +++ b/tests/wave2/fixtures/model-scope/golden-v0.27.0/add-linebreaks.py @@ -0,0 +1,13 @@ +# Put "\n" between the bracket groups of every stored XpathConstraint of the two named microflows, +# through the toolkit's mxpatch library (unit file + ContentsHash row in one step). +import sys, os +sys.path.insert(0, sys.argv[1]); import mxpatch +m = mxpatch.Model(sys.argv[2]) +for mod, name in (("MyFirstModule", "ACT_Item_Fetch"), ("Other", "ACT_Thing_Fetch")): + u = m.find_unit("Microflows$Microflow", name, module=mod) + for node in mxpatch.walk(u.doc): + for i, (t, k, v) in enumerate(node): + if k == "XpathConstraint" and t == 0x02 and "][" in v: + node[i] = (t, k, v.replace("][", "]\n[")) + print(mod, name, repr(v), "->", repr(node[i][2])) + print(m.write_unit(u, apply=True)) diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/after/TestApp.mpr.sql b/tests/wave2/fixtures/model-scope/golden-v0.27.0/after/TestApp.mpr.sql new file mode 100644 index 00000000..a7567dc7 --- /dev/null +++ b/tests/wave2/fixtures/model-scope/golden-v0.27.0/after/TestApp.mpr.sql @@ -0,0 +1,54 @@ +BEGIN TRANSACTION; +CREATE TABLE Unit + ( + UnitID BLOB PRIMARY KEY NOT NULL, + ContainerID BLOB, + ContainmentName TEXT, + TreeConflict LONG, + ContentsHash TEXT, + ContentsConflicts TEXT + ); +INSERT INTO "Unit" VALUES(X'52F04478EFE13543A8334FE39C4B63A5',X'52F04478EFE13543A8334FE39C4B63A5','',0,'1P8B8tZuK5hIRdTwEndlcERwqbt6eN6MCgMueh/qg9U=',''); +INSERT INTO "Unit" VALUES(X'3D06704EE1537A47AB926B763330C293',X'52F04478EFE13543A8334FE39C4B63A5','ProjectDocuments',0,'VrvS4YNiyxgp52vATpJ8mD7MRUed7rO1uWLGJrHCe4M=',''); +INSERT INTO "Unit" VALUES(X'AB8629B6C91E845C9D331F0829FFCE9B',X'52F04478EFE13543A8334FE39C4B63A5','ProjectDocuments',0,'CQW8g4stdHD3myfUj1t55R6glTPOaexTBbw4UABJ5IY=',''); +INSERT INTO "Unit" VALUES(X'B5ECD2572F6CFA4FA0B3A37A40DA3ADF',X'52F04478EFE13543A8334FE39C4B63A5','ProjectDocuments',0,'q2Ut4uuc1d9aLWNnsLEKrYiDAEPyZDPMtJWjs/E5HfI=',''); +INSERT INTO "Unit" VALUES(X'BD656FBD93239D5393884684A54D0646',X'52F04478EFE13543A8334FE39C4B63A5','ProjectDocuments',0,'VGLSsTp4/eturnjDqpHoM7PTbTOYHIyo/2VPX6/pqSE=',''); +INSERT INTO "Unit" VALUES(X'C67D7416B7B6AE42AABF255DCA2AEEAF',X'52F04478EFE13543A8334FE39C4B63A5','Modules',0,'RcBxkbGB/tK5dH1fTyFD01HFb73PRmtvZ16Rjm4biTY=',''); +INSERT INTO "Unit" VALUES(X'B375844ACB140041896358F2FD3A0BE8',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'V7yfLmuC/7BpAZpws/8y8gwe7I50gvOLNGGE/McILeA=',''); +INSERT INTO "Unit" VALUES(X'E9062165383FC746BCC42B4E4AD39160',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'38VTr29tSOGyWX/lt3RiOSq7QQa0jQbs8ZJ2E62YCNY=',''); +INSERT INTO "Unit" VALUES(X'FC05685CDFCA724DA5EFF25DA7CF4C97',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'BhMyTju/Rcl2MctB67zt+opPQO9Z+2qQL72RrAuAdfQ=',''); +INSERT INTO "Unit" VALUES(X'3521F934655CC1409AF9166186ECF501',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'wsCVL2KDvUOIPIeunloMOIbGmY/JQc7L/NVeFAxeT98=',''); +INSERT INTO "Unit" VALUES(X'30B1E8E1BA26BD43B2E70F6B61A61008',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'8OydqzRphNoPJR7gThBXnL9jfK1KBaSfhUsIv4I5XKY=',''); +INSERT INTO "Unit" VALUES(X'10BB28F93218C84994157E24495EC970',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'fg5TRQPPXcJl0T2VGKQ9C4trQ+0rQLdlgKVwStzWTRI=',''); +INSERT INTO "Unit" VALUES(X'F7F1166E0712704D8847C369A7DD360A',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'hfM1rUN9AgT3QqkZjG80Uo1ei49uDR4qAVB1QJmDg3A=',''); +INSERT INTO "Unit" VALUES(X'10667784B213CB48A265D5E984D63129',X'C67D7416B7B6AE42AABF255DCA2AEEAF','DomainModel',0,'ACeKqqSV1sq4QqcYapYZQmcVRv89xyPnRckm6WfTP2g=',''); +INSERT INTO "Unit" VALUES(X'37EE21D102F1AE56B644160F1D340296',X'C67D7416B7B6AE42AABF255DCA2AEEAF','ModuleSettings',0,'KYFxFtYHsM09/IpsXqRyxqMOeRhNe+vDbUwNNf2VpTQ=',''); +INSERT INTO "Unit" VALUES(X'A2BA643F5CB899499A3C3CC38382EB54',X'C67D7416B7B6AE42AABF255DCA2AEEAF','ModuleSecurity',0,'gaKtd2z3+JUzt0UL2ThGw0i51m6DwUGJwktA0h727tc=',''); +INSERT INTO "Unit" VALUES(X'2328CC1F03C3275AB6C778B04C96C88F',X'52F04478EFE13543A8334FE39C4B63A5','ProjectConversion',0,'Ach7bT0Xt73Hw9ArP9izw30C89fTuCrhIJMjYmXtt4A=',''); +INSERT INTO "Unit" VALUES(X'A7EB1C6CE0344C4DB6AEC54B487A3562',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'SItwWFUvsSV4dVzpliS/O9c7MqrwoKrUEAdfc0s7288=',''); +INSERT INTO "Unit" VALUES(X'73AF86552C67184A9CE48A9C0F092EF3',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'cDRtdwtcwTmrrv6RmrM68bL3oLxOrGLkHbIZUykNxac=',''); +INSERT INTO "Unit" VALUES(X'15FF067DEF6F8748A15A4230D851A87D',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'BAZHNfXIMeCgZMn8nP9592rOFYear/tz1l9BbrqqiN4=',''); +INSERT INTO "Unit" VALUES(X'60E4D3253DC59E41A2A9E1428F40B14F',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'ugRNWwcTrOfJ7Dp13o/vxoJD3lJBtEQ2s1eD/dUJ5D4=',''); +INSERT INTO "Unit" VALUES(X'2E6F275BD5EEA94F903B3483792CBEA7',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'MWNqVaPXrQYZ82Hu6APzlO//oCzKOyhN2dCxEP73OU8=',''); +INSERT INTO "Unit" VALUES(X'7990D298071E3541BB38598E72EB0A0D',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'CCmrWhJ6VT29aa4x6I8Dc3hAvB7mkohqzIkL3GhxaLw=',''); +INSERT INTO "Unit" VALUES(X'B708DDC4C02DC94C823C3E612EB3A281',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'a+rm/AKPx584tpcvBBRHsdyBe4861T6Wdxmi3S1Boys=',''); +INSERT INTO "Unit" VALUES(X'36646D7ABEA015419B12ABDB05CFE1CC',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'ySC1oRci5dMZBAyD+gRHutR3AGzVoQM/T5KWocVkn04=',''); +INSERT INTO "Unit" VALUES(X'EA396B05142E124D8AE187FA4B3C038A',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'Ux6EeBbxCPMJjiYdq0Sn0OlMgYWnqkD+d9j4yO9t3Zg=',''); +INSERT INTO "Unit" VALUES(X'47834DC5ECA8BA40B88E873BFEC0A493',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'cDw61j1j1+oCzSVuMI4E6gSiFRP7VPfTB2Gc0zM8xKo=',''); +INSERT INTO "Unit" VALUES(X'69A93CF850FF5B4EB61DB1FFE55E03D9',X'52F04478EFE13543A8334FE39C4B63A5','Modules',0,'+gF5p3ewY7rqCSllXG/DdweXgAB6KvBRe/wAy+C1kA8=',''); +INSERT INTO "Unit" VALUES(X'17518D41452D054F8F3479AF8E976733',X'69A93CF850FF5B4EB61DB1FFE55E03D9','DomainModel',0,'UfrAKifyHT2C5DoiaWrqOm4Qnlrdc9DI1gh11FYxsoA=',''); +INSERT INTO "Unit" VALUES(X'D910A9F60640064F9D8044069877CFAA',X'69A93CF850FF5B4EB61DB1FFE55E03D9','ModuleSecurity',0,'Zy21mHbAOhuiM0mDsnUHSNruQSlSOl+QIp9l46dnIWM=',''); +INSERT INTO "Unit" VALUES(X'902D5346D18BA64CB9CF77B2431AF10E',X'69A93CF850FF5B4EB61DB1FFE55E03D9','ModuleSettings',0,'cDV0rg4Hnr/BJ/ULUYYWjO7gxSKk0O+cca0dZWBAEp0=',''); +INSERT INTO "Unit" VALUES(X'A800E9C2876B1649AE7B49FED7F0A939',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'ahBVqDjFG06CdIFi1oVERkNhqzsu1BooguwrkzjPzhQ=',''); +INSERT INTO "Unit" VALUES(X'441A32CE0311E94E875B6D521BDD3178',X'69A93CF850FF5B4EB61DB1FFE55E03D9','Documents',0,'SvHKySsuRFADgp+e4dqURmUJssS6BmadPc30Lnbbh7E=',''); +CREATE TABLE _MetaData + ( + _FormatVersion INTEGER, + _ProductVersion TEXT, + _BuildVersion TEXT, + _SchemaHash TEXT + ); +INSERT INTO "_MetaData" VALUES(2,'11.14.0','11.14.0','{SHA256}o9B9S8lorV9RD5gY9B6j1bJp4ALW87u4newnreIbRAg='); +CREATE TABLE _Transaction (LastTransactionID TEXT); +INSERT INTO "_Transaction" VALUES('84d3ba6a-1657-45ac-83a3-222e6d0120b7'); +COMMIT; diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/after/mprcontents/84/77/84776610-13b2-48cb-a265-d5e984d63129.mxunit b/tests/wave2/fixtures/model-scope/golden-v0.27.0/after/mprcontents/84/77/84776610-13b2-48cb-a265-d5e984d63129.mxunit new file mode 100644 index 0000000000000000000000000000000000000000..ee74f2c092d5b33cd00050ff426e09791a580423 GIT binary patch literal 5752 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 literal 0 HcmV?d00001 diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/TestApp.mpr.sql b/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/TestApp.mpr.sql new file mode 100644 index 00000000..32eff5d5 --- /dev/null +++ b/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/TestApp.mpr.sql @@ -0,0 +1,54 @@ +BEGIN TRANSACTION; +CREATE TABLE Unit + ( + UnitID BLOB PRIMARY KEY NOT NULL, + ContainerID BLOB, + ContainmentName TEXT, + TreeConflict LONG, + ContentsHash TEXT, + ContentsConflicts TEXT + ); +INSERT INTO "Unit" VALUES(X'52F04478EFE13543A8334FE39C4B63A5',X'52F04478EFE13543A8334FE39C4B63A5','',0,'1P8B8tZuK5hIRdTwEndlcERwqbt6eN6MCgMueh/qg9U=',''); +INSERT INTO "Unit" VALUES(X'3D06704EE1537A47AB926B763330C293',X'52F04478EFE13543A8334FE39C4B63A5','ProjectDocuments',0,'VrvS4YNiyxgp52vATpJ8mD7MRUed7rO1uWLGJrHCe4M=',''); +INSERT INTO "Unit" VALUES(X'AB8629B6C91E845C9D331F0829FFCE9B',X'52F04478EFE13543A8334FE39C4B63A5','ProjectDocuments',0,'CQW8g4stdHD3myfUj1t55R6glTPOaexTBbw4UABJ5IY=',''); +INSERT INTO "Unit" VALUES(X'B5ECD2572F6CFA4FA0B3A37A40DA3ADF',X'52F04478EFE13543A8334FE39C4B63A5','ProjectDocuments',0,'q2Ut4uuc1d9aLWNnsLEKrYiDAEPyZDPMtJWjs/E5HfI=',''); +INSERT INTO "Unit" VALUES(X'BD656FBD93239D5393884684A54D0646',X'52F04478EFE13543A8334FE39C4B63A5','ProjectDocuments',0,'VGLSsTp4/eturnjDqpHoM7PTbTOYHIyo/2VPX6/pqSE=',''); +INSERT INTO "Unit" VALUES(X'C67D7416B7B6AE42AABF255DCA2AEEAF',X'52F04478EFE13543A8334FE39C4B63A5','Modules',0,'RcBxkbGB/tK5dH1fTyFD01HFb73PRmtvZ16Rjm4biTY=',''); +INSERT INTO "Unit" VALUES(X'B375844ACB140041896358F2FD3A0BE8',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'V7yfLmuC/7BpAZpws/8y8gwe7I50gvOLNGGE/McILeA=',''); +INSERT INTO "Unit" VALUES(X'E9062165383FC746BCC42B4E4AD39160',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'38VTr29tSOGyWX/lt3RiOSq7QQa0jQbs8ZJ2E62YCNY=',''); +INSERT INTO "Unit" VALUES(X'FC05685CDFCA724DA5EFF25DA7CF4C97',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'BhMyTju/Rcl2MctB67zt+opPQO9Z+2qQL72RrAuAdfQ=',''); +INSERT INTO "Unit" VALUES(X'3521F934655CC1409AF9166186ECF501',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'wsCVL2KDvUOIPIeunloMOIbGmY/JQc7L/NVeFAxeT98=',''); +INSERT INTO "Unit" VALUES(X'30B1E8E1BA26BD43B2E70F6B61A61008',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'8OydqzRphNoPJR7gThBXnL9jfK1KBaSfhUsIv4I5XKY=',''); +INSERT INTO "Unit" VALUES(X'10BB28F93218C84994157E24495EC970',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'fg5TRQPPXcJl0T2VGKQ9C4trQ+0rQLdlgKVwStzWTRI=',''); +INSERT INTO "Unit" VALUES(X'F7F1166E0712704D8847C369A7DD360A',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'hfM1rUN9AgT3QqkZjG80Uo1ei49uDR4qAVB1QJmDg3A=',''); +INSERT INTO "Unit" VALUES(X'10667784B213CB48A265D5E984D63129',X'C67D7416B7B6AE42AABF255DCA2AEEAF','DomainModel',0,'Zhfdql1eIs9ODFLHZZIQsReqGCuKso6oouL/2JXvcSc=',''); +INSERT INTO "Unit" VALUES(X'37EE21D102F1AE56B644160F1D340296',X'C67D7416B7B6AE42AABF255DCA2AEEAF','ModuleSettings',0,'KYFxFtYHsM09/IpsXqRyxqMOeRhNe+vDbUwNNf2VpTQ=',''); +INSERT INTO "Unit" VALUES(X'A2BA643F5CB899499A3C3CC38382EB54',X'C67D7416B7B6AE42AABF255DCA2AEEAF','ModuleSecurity',0,'gaKtd2z3+JUzt0UL2ThGw0i51m6DwUGJwktA0h727tc=',''); +INSERT INTO "Unit" VALUES(X'2328CC1F03C3275AB6C778B04C96C88F',X'52F04478EFE13543A8334FE39C4B63A5','ProjectConversion',0,'Ach7bT0Xt73Hw9ArP9izw30C89fTuCrhIJMjYmXtt4A=',''); +INSERT INTO "Unit" VALUES(X'A7EB1C6CE0344C4DB6AEC54B487A3562',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'SItwWFUvsSV4dVzpliS/O9c7MqrwoKrUEAdfc0s7288=',''); +INSERT INTO "Unit" VALUES(X'73AF86552C67184A9CE48A9C0F092EF3',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'cDRtdwtcwTmrrv6RmrM68bL3oLxOrGLkHbIZUykNxac=',''); +INSERT INTO "Unit" VALUES(X'15FF067DEF6F8748A15A4230D851A87D',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'BAZHNfXIMeCgZMn8nP9592rOFYear/tz1l9BbrqqiN4=',''); +INSERT INTO "Unit" VALUES(X'60E4D3253DC59E41A2A9E1428F40B14F',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'ugRNWwcTrOfJ7Dp13o/vxoJD3lJBtEQ2s1eD/dUJ5D4=',''); +INSERT INTO "Unit" VALUES(X'2E6F275BD5EEA94F903B3483792CBEA7',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'MWNqVaPXrQYZ82Hu6APzlO//oCzKOyhN2dCxEP73OU8=',''); +INSERT INTO "Unit" VALUES(X'7990D298071E3541BB38598E72EB0A0D',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'CCmrWhJ6VT29aa4x6I8Dc3hAvB7mkohqzIkL3GhxaLw=',''); +INSERT INTO "Unit" VALUES(X'B708DDC4C02DC94C823C3E612EB3A281',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'a+rm/AKPx584tpcvBBRHsdyBe4861T6Wdxmi3S1Boys=',''); +INSERT INTO "Unit" VALUES(X'36646D7ABEA015419B12ABDB05CFE1CC',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'ySC1oRci5dMZBAyD+gRHutR3AGzVoQM/T5KWocVkn04=',''); +INSERT INTO "Unit" VALUES(X'EA396B05142E124D8AE187FA4B3C038A',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'Ux6EeBbxCPMJjiYdq0Sn0OlMgYWnqkD+d9j4yO9t3Zg=',''); +INSERT INTO "Unit" VALUES(X'47834DC5ECA8BA40B88E873BFEC0A493',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'cDw61j1j1+oCzSVuMI4E6gSiFRP7VPfTB2Gc0zM8xKo=',''); +INSERT INTO "Unit" VALUES(X'69A93CF850FF5B4EB61DB1FFE55E03D9',X'52F04478EFE13543A8334FE39C4B63A5','Modules',0,'+gF5p3ewY7rqCSllXG/DdweXgAB6KvBRe/wAy+C1kA8=',''); +INSERT INTO "Unit" VALUES(X'17518D41452D054F8F3479AF8E976733',X'69A93CF850FF5B4EB61DB1FFE55E03D9','DomainModel',0,'UfrAKifyHT2C5DoiaWrqOm4Qnlrdc9DI1gh11FYxsoA=',''); +INSERT INTO "Unit" VALUES(X'D910A9F60640064F9D8044069877CFAA',X'69A93CF850FF5B4EB61DB1FFE55E03D9','ModuleSecurity',0,'Zy21mHbAOhuiM0mDsnUHSNruQSlSOl+QIp9l46dnIWM=',''); +INSERT INTO "Unit" VALUES(X'902D5346D18BA64CB9CF77B2431AF10E',X'69A93CF850FF5B4EB61DB1FFE55E03D9','ModuleSettings',0,'cDV0rg4Hnr/BJ/ULUYYWjO7gxSKk0O+cca0dZWBAEp0=',''); +INSERT INTO "Unit" VALUES(X'A800E9C2876B1649AE7B49FED7F0A939',X'C67D7416B7B6AE42AABF255DCA2AEEAF','Documents',0,'5hICrje06AnIUhcoDiKHghDWRG4G9VpRwx4VwmKMUHM=',''); +INSERT INTO "Unit" VALUES(X'441A32CE0311E94E875B6D521BDD3178',X'69A93CF850FF5B4EB61DB1FFE55E03D9','Documents',0,'bhGycGJYKS8TDXQJPw9fHyfng5q24GCMnCmx+f1eCsU=',''); +CREATE TABLE _MetaData + ( + _FormatVersion INTEGER, + _ProductVersion TEXT, + _BuildVersion TEXT, + _SchemaHash TEXT + ); +INSERT INTO "_MetaData" VALUES(2,'11.14.0','11.14.0','{SHA256}o9B9S8lorV9RD5gY9B6j1bJp4ALW87u4newnreIbRAg='); +CREATE TABLE _Transaction (LastTransactionID TEXT); +INSERT INTO "_Transaction" VALUES('e6e3e92d-826a-435c-9284-7c5044068c2f'); +COMMIT; diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/16/74/16747dc6-b6b7-42ae-aabf-255dca2aeeaf.mxunit b/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/16/74/16747dc6-b6b7-42ae-aabf-255dca2aeeaf.mxunit new file mode 100644 index 0000000000000000000000000000000000000000..bd87fe168276f2c83d4e9622229ae268faf8d185 GIT binary patch literal 236 zcmaFEz`($&;_1R50Aw&6t1S`RzHOb;s{N|5r?lR!XHp5NEJ$S#0ZIiFvp6IpH8&OF1fXSpiMgo^d_Ze`E8Q}Sic6ppjDD%*!TCic Po_Q&$6(CoEfCB>n5UWJQ literal 0 HcmV?d00001 diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/84/77/84776610-13b2-48cb-a265-d5e984d63129.mxunit b/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/84/77/84776610-13b2-48cb-a265-d5e984d63129.mxunit new file mode 100644 index 0000000000000000000000000000000000000000..6250132a9a88902a285a0b09cf093f62291fe903 GIT binary patch literal 5751 zcmd5=2~ZPf6#g56AnI_5h~NRGqmHATB2|Py5+RZVC4l19uH*+}lU>Yij8Rl7(W*to z8!td~JjSzLBZ{>iRnhUNw^GFe>rn;Usnw2R`)9Ku*%&ZXGh=2l$?pF1{`Y_Hz3+Q( zwH*L(4i*W32S6W>Y^{Em_nD+}c;&JFYFJNBaGEX`0zdRfK&UVUE+OPlNd{Y70B$^v z6BI@%2%H3M4MswNlff7_o+ODZg{>!iH9?Y1KIRCBER71{Mx)>}+L#ch6qEvz;J8oY z7EWPcyo;s(DRFmSK3qRoqOa0RA8DU$Wc=@lELgZq2GhoLB2}&ud6|_3NitQVM7G)P zQ*Lu%*lR04Pd>rAVTtZEW z;Y6qe=u99F3C1)w8YN7r+^mxA(k@rO_MT;$>X zAcXNR5G`V*nG7Wmp2G&yOAPTGY8*I)7SkUF+zfPK4%vns(FZg49pLXQ2}*FDI`>}7 z!KJGPVH;tEZRcq5#w|0vr9}e=m6opSKg?m+*7Z8#KVxx2!e%^s=&kY9Z_L=z>1s|^ zHnz9caw;ds`Xv<~a*?zN`MTZ5$2PW)vgTx(OTzYtJ$&>jmrc=ohvb@0TobmDU^FXl zbpf6VVU>!vDEwIGm&ETK9fqyf^aj1(-jamk?MEW+#Z;skv2{Z!L}N==Z6g`5@g0A8 z{LK2FeG<#PE;TG&!@cu*I5-^iIAT8^(xqT~uEB4uDxwH@Ty zX~AOOSFxG=&AVn!_wD|q2S=pBa-dOLoaz4pjKYi+$ra&pILS&5IAS7;mNJ@g!@@%c zghwKwlVG~BFcIQVjVTqAjSI?IrF`P?rrkw+=Y)f?ezQ)zyimu$(FHAHk%g47GUJ+r zk^FHOo&)6xIs=ISU~vn%3oUM{A-a~cZ>DIGTg;jvP)#aG3LC359NL&KT;u{DQv){{ zp;2c+z(usIkke;)R}Kv<9fV}i%u$EZ6JGVL_Dot>HUIeaqTzr!*awSkCC5^1rK}2@ z;a^)ar9M!w=}{-yhGoYOGkbn~u}$n-?;5%F-@%*rZs^g__kqTUm|1KEX8sbf!mX}r zcgDBpAL9LPf$bDO)$gja!phd3o zzu)1N_)nZ?3WLZez*vZXK!$;`oEXYNoM^#F(V`Q>_80yoEk9dX*-J~UY${opS}{%m zj3tio*ED(nZ)91b8>CQ^6w*{Q9rU3$ejiCj0ADoxm`SoK#Jgo1l270ioJ0Y(21Owa3JVRjnR7OEiQQuMp4Tzy1o!%&C7|s7 zv2zh^?T<2BOSp~wEeYg^TbS_=&1UH=rYZDrpW%OTi=Z%I));NsB-3ox9CgzJj-kHN z6co~}#*T(Ph{$`-aqrQqy;Q&VV*c&wIIpInZi^y3EW2@A#cQ)WzZ7@Au52pc=M%as zd9Awr&Ft9X4#$qZ#Tqq4G4G!gt&^_l8Misbx7h~SB(r9EDN}C&oFYMM6iRbtL}Yr7 z*mkuIf3@G;aSJ<6+-i4a7)U@wL6ucGL`S!<9N2Bz%WVC~A4eDS4=jq&9-FYo$Nq9~ zT$^TDT0W>-GnZBxtF(4jbRS*wA|$zZ$6TRj%&!5A@9!-w1Co67MyILDDhhk@&Bpbm z@$?vc{F7Y*0R(rIEZHh9sDr{0Zxl1~wd(d-HiOYpi-NIuEm>nHaS=OK# z8r%#Pto?YB;ewzagu8xozmc=1_O!jQ8eZSX(90_g*%Yul*QH~!X_1nJ9yiy!pDQ}+ zcwqY^Es38M6+C#(lG3;5;L11$0&5MM;T;4U*rlhxD(O061#hwJruT}~Z=zcQY&xzX zp~agSwie{8GCEhC>Mz-R`r7iIr=p%X9@sGpZWJzdZ4j^G7R;&L_vLd30&5MMU0PsG Kv!8+%0Pr8H6Gkxr literal 0 HcmV?d00001 diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/c2/e9/c2e900a8-6b87-4916-ae7b-49fed7f0a939.mxunit b/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/c2/e9/c2e900a8-6b87-4916-ae7b-49fed7f0a939.mxunit new file mode 100644 index 0000000000000000000000000000000000000000..745d4cc2d876efe5c9d7c6eb1417fa3a2a7a2431 GIT binary patch literal 2596 zcmd5;TTC2P7(Sg{FXU=Td zs*xB&Q%uxGZK6$s?UTKzy@2(lRx62Gk(b(-G??g<#du3E#>NKy&&=$ygAk+f!AW*z z_MHFx-+%r7@#GIKL}i%Cnu?ZiwSk}ZP;pI`UZQmNukTxpA zzQ1U5TPr%c>O!cn48a|_HlhJ|%mrcUwrlL0o>k4`(JdX{J$Un!4N~L;i7-rAFaa(i zp@k^mXM!|=7Yf;?$*`CM;@+~d3iu%*BJUyS4s!(-0Mm=U5Hc}qm;d2SjE>Zw&0`kU57N&U}#H`}*GS+bbu} z)=zx4V`1}=%PgkA%wfab2{l!cyi^!xjc zS`7U#l)Sv_k-+e|YX=S&;Wrn_0~s)gDiW>_WreBSBdcbo6KZkieA>tg5FOHP5}-qH zeyW2~{QD?B@OPox`tG9xAZixz`!` z8nQ&z4;>lZ=k|P35c~h6ZhiFQ{I4&xSD&E;41X^@`HxeF zT7SU6LYMewkP=@6eWX9_wYM%Tl8#GIqzrA z&nZ;V21tE(*)+GED%P7D-Xa83F}4j;_zYu&I=#qCN8B3OA+=jWc{?=P$3NpT*0NxP d!E^{o7LnbWKDH*5HBo(R-PP$^({~<3e*%rU{YL-* literal 0 HcmV?d00001 diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/ce/32/ce321a44-1103-4ee9-875b-6d521bdd3178.mxunit b/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/ce/32/ce321a44-1103-4ee9-875b-6d521bdd3178.mxunit new file mode 100644 index 0000000000000000000000000000000000000000..88407505d456bc23e1742bd1f00c84f3cf4380b2 GIT binary patch literal 2588 zcmd5;eM}o=7=L#ySYdO>K;~ouOBb9b3$O?pTt-U^R6dGr2Sl>K+q;)?aNL#m?w~+o z*mNrDra_jO7&nQ|sq+tYbJ0cA#gCYv#EEf{#26Dda5H`^TbAf_KKIems}POx51aIQ zectDNe$VIcee*LAN^|-=C=0>Iv#xB$cK!EF54HCNZu+`3#yCSmQG#+|sgY-?)FnuR zvNQ3Dm?qpu^sTPyP)Den7b6`tL}9xT~J0C4*IGWyha_+r5Pn#e&SV4jcriym}LPSh} z2oUE0X@n>Q(k$^HbbxwUNl7Jq5fkKx2};)`AOOrV^Z}EMIkP$aRA$dM*VHdz+oa3 zZY?Z!74OJsuMu#hun7LSi+3Pf6B!IbgM%3vwORa`kl!l1Wt-pi*mL_YH=j8-X!^6( z>Q8)g11~Gc9{;CD3~dXoqkW+7WK~dFFy(PrAo?(B#n!VS!@zlXSzFKst{O*^CefuT zQY{e)#R^HRFm9+@gi5N&!2n8BZ($GV!ho^lKqK20v=8tWlihM@eO~s5-r7AEr|_iX z@Xuy&IiQqa4cX}xxxX2l@?{1GB34u?wqQXe2B|DXG=OHW(WnaTvW)qgmKMBFcOm%u zli$`pc;w#0Oip;kGKC=hDiK+t*+Xf5%13xnA8EXTqvUI5x zS+;bkZT%mYu4vWl3kA1lHcd{vtv>Q_QS|>)y2xPdeWUq#RY%0DzddmC`8Zkq^U@(( zV(yL6)enlTmi~C<`Pc7mEx4}!XintHM;AkrX83DBNjm_v9kR1U;yHaUax`Mv>yo$2 zIkG2j2$EuXV!S0O_ zRyA1DmXHqV&DjVBF`PAZmPj^Mp7>Gm=bD3Xzy1JGCdW`xMfb zK-T72syj64X!Y7I`}w)$WDITnq7+Ws&*Z(`1FVNRoUoIS@!$- literal 0 HcmV?d00001 diff --git a/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/f8/3c/f83ca969-ff50-4e5b-b61d-b1ffe55e03d9.mxunit b/tests/wave2/fixtures/model-scope/golden-v0.27.0/before/mprcontents/f8/3c/f83ca969-ff50-4e5b-b61d-b1ffe55e03d9.mxunit new file mode 100644 index 0000000000000000000000000000000000000000..c893f6b1eed871c3a0ae6a815c6abad793978d03 GIT binary patch literal 228 zcmaFDz`($&;_1R50Aw&^uC)0P@ITsbo9xE_Pve+xGO2`A7NjzW0Hp$o^0QKtONv!| z^HWN5Qay7Eau^u>Qp1sV?k noise + literal whitespace inside a quoted XPath literal changed -> real + wsfield XPath whitespace AND a Caption change -> real + field only a Caption change -> real + reorder two keys swapped -> real + +Units (container in brackets): Project P, ModuleA, ModuleB [P]; folder Sub [ModuleA]; +MfA1 [Sub] two XPaths, PgA [ModuleA] page data source key 'XPathConstraint', MfA2 [ModuleA] none, +MfB1, MfB2 [ModuleB] XPaths, Nav [P] none. UnitIDs are uuid5 of the name, so runs are repeatable. +""" +import base64 +import hashlib +import importlib.util +import os +import sqlite3 +import subprocess +import sys +import uuid + +HERE = os.path.dirname(os.path.abspath(__file__)) +PB = os.path.abspath(os.path.join(HERE, "..", "..", "..", "..", "project-bin")) + + +def _load(name): + spec = importlib.util.spec_from_file_location(name, os.path.join(PB, name + ".py")) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +bson = _load("mxunit_bson") +NS = uuid.UUID("6f1c1c1e-0000-4000-8000-000000000001") +XP_OLD = "[Label = 'A']\n[Flag != 'B']" # verbatim from golden-v0.27.0/ (before / after the rename) +XP_NEW = "[Label = 'A'][Flag != 'B']" +LIT_OLD = "[Note = 'two words']\n[Open = true]\n" +LIT_NEW = "[Note = 'two words'][Open = true]" + + +def uid(name): + return uuid.uuid5(NS, name) + + +def S(k, v): + return (0x02, k, v) + + +def D(k, v): + return (0x03, k, v) + + +def A(k, items): + return (0x04, k, [(0x10, "0", 3)] + [(0x03, str(i + 1), d) for i, d in enumerate(items)]) + + +def mf(name, xpaths): + objs = [] + for x in xpaths: + objs.append([S("$Type", "Microflows$ActionActivity"), + D("Action", [S("$Type", "Microflows$RetrieveAction"), + D("RetrieveSource", [S("$Type", "Microflows$DatabaseRetrieveSource"), + S("XpathConstraint", x)])])]) + return [S("$Type", "Microflows$Microflow"), S("Name", name), + D("ObjectCollection", [S("$Type", "Microflows$MicroflowObjectCollection"), A("Objects", objs)]), + S("Caption", name + " caption"), S("Documentation", "")] + + +def page(name, x): + return [S("$Type", "Pages$Page"), S("Name", name), S("Caption", name), + D("Content", [D("DataSource", [S("$Type", "Pages$ListViewXPathSource"), S("XPathConstraint", x)])])] + + +UNITS = [ # name, container, containment, doc + ("P", "P", "", [S("$Type", "Projects$Project"), S("Name", "P")]), + ("ModuleA", "P", "Modules", [S("$Type", "Projects$ModuleImpl"), S("Name", "ModuleA")]), + ("ModuleB", "P", "Modules", [S("$Type", "Projects$ModuleImpl"), S("Name", "ModuleB")]), + ("Sub", "ModuleA", "Folders", [S("$Type", "Projects$Folder"), S("Name", "Sub")]), + ("MfA1", "Sub", "Documents", mf("MfA1", [XP_OLD, LIT_OLD])), + ("PgA", "ModuleA", "Documents", page("PgA", XP_OLD)), + ("MfA2", "ModuleA", "Documents", mf("MfA2", [])), + ("MfB1", "ModuleB", "Documents", mf("MfB1", [XP_OLD])), + ("MfB2", "ModuleB", "Documents", mf("MfB2", [XP_OLD, XP_OLD])), + ("Nav", "P", "ProjectDocuments", [S("$Type", "Navigation$NavigationDocument"), S("Name", "Nav")]), +] + + +def h(data): + return base64.b64encode(hashlib.sha256(data).digest()).decode() + + +def paths(root): + mpr = [f for f in os.listdir(root) if f.endswith(".mpr")][0] + return os.path.join(root, mpr), os.path.join(root, "mprcontents") + + +def unit_file(root, name): + u = str(uid(name)) + return os.path.join(paths(root)[1], u[:2], u[2:4], u + ".mxunit") + + +def model_root(top): + return os.path.join(top, "app") if os.path.isdir(os.path.join(top, "app")) else top + + +def git(top, *a): + subprocess.check_call(["git", "-c", "user.email=t@t", "-c", "user.name=t"] + list(a), cwd=top, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def put(root, name, doc, container, containment, db): + p = unit_file(root, name) + os.makedirs(os.path.dirname(p), exist_ok=True) + raw = bson.enc_doc(doc) + with open(p, "wb") as fh: + fh.write(raw) + db.execute("INSERT OR REPLACE INTO Unit VALUES (?,?,?,?,?,?)", + (uid(name).bytes_le, uid(container).bytes_le, containment, 0, h(raw), "")) + + +def cmd_init(top, sub): + os.makedirs(top, exist_ok=True) + root = os.path.join(top, sub) if sub else top + os.makedirs(root, exist_ok=True) + subprocess.check_call(["git", "init", "-q", top]) + db = sqlite3.connect(os.path.join(root, "App.mpr")) + db.execute("CREATE TABLE _MetaData (_FormatVersion INTEGER, _ProductVersion TEXT, _BuildVersion TEXT, _SchemaHash TEXT)") + db.execute("INSERT INTO _MetaData VALUES (2,'10.24.18','10.24.18.1','x')") + db.execute("CREATE TABLE Unit (UnitID BLOB PRIMARY KEY NOT NULL, ContainerID BLOB, ContainmentName TEXT, " + "TreeConflict LONG, ContentsHash TEXT, ContentsConflicts TEXT)") + for name, cont, containment, doc in UNITS: + put(root, name, doc, cont, containment, db) + db.commit() + db.close() + git(top, "add", "-A") + git(top, "commit", "-qm", "model") + + +def mutate(doc, mode): + """In place; returns True when something changed.""" + changed = False + for i, (t, k, v) in enumerate(doc): + if t in (0x03, 0x04): + changed |= mutate(v, mode) + elif k.lower() == "xpathconstraint" and t == 0x02: + if mode in ("ws", "wsfield") and v == XP_OLD: + doc[i] = (t, k, XP_NEW) + changed = True + elif mode == "literal" and v == LIT_OLD: + doc[i] = (t, k, LIT_NEW) + changed = True + elif k == "Caption" and mode in ("wsfield", "field") and t == 0x02: + doc[i] = (t, k, v + " changed") + changed = True + if mode == "reorder" and len(doc) >= 4 and doc[0][1] == "$Type" and doc[-1][1] == "Documentation": + doc[-2], doc[-1] = doc[-1], doc[-2] + changed = True + return changed + + +def cmd_edit(root, name, mode): + p = unit_file(root, name) + doc = bson.load(p) + if not mutate(doc, mode): + sys.exit("mode %s changes nothing in %s" % (mode, name)) + raw = bson.enc_doc(doc) + with open(p, "wb") as fh: + fh.write(raw) + db = sqlite3.connect(paths(root)[0]) + db.execute("UPDATE Unit SET ContentsHash=? WHERE UnitID=?", (h(raw), uid(name).bytes_le)) + db.commit() + db.close() + + +def cmd_delete(root, name): + os.remove(unit_file(root, name)) + db = sqlite3.connect(paths(root)[0]) + db.execute("DELETE FROM Unit WHERE UnitID=?", (uid(name).bytes_le,)) + db.commit() + db.close() + + +def cmd_add(root, name, module): + db = sqlite3.connect(paths(root)[0]) + put(root, name, mf(name, []), module, "Documents", db) + db.commit() + db.close() + + +def cmd_hashes(root): + db = sqlite3.connect(paths(root)[0]) + bad = [] + for u, hh in db.execute("SELECT UnitID, ContentsHash FROM Unit"): + s = str(uuid.UUID(bytes_le=bytes(u))) + p = os.path.join(paths(root)[1], s[:2], s[2:4], s + ".mxunit") + if os.path.isfile(p) and h(open(p, "rb").read()) != hh: + bad.append(s) + print("clean" if not bad else " ".join(sorted(bad))) + + +def main(a): + cmd, top = a[0], a[1] + if cmd == "init": + return cmd_init(top, a[3] if len(a) > 3 and a[2] == "--sub" else "") + root = model_root(top) + if cmd == "edit": + return cmd_edit(root, a[2], a[3]) + if cmd == "delete": + return cmd_delete(root, a[2]) + if cmd == "add": + return cmd_add(root, a[2], a[3]) + if cmd == "hashes": + return cmd_hashes(root) + sys.exit("unknown command " + cmd) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tests/wave2/test-model-scope.sh b/tests/wave2/test-model-scope.sh new file mode 100755 index 00000000..174e8f53 --- /dev/null +++ b/tests/wave2/test-model-scope.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# test-model-scope.sh -- project-bin/model-scope.py (report / restore / check --staged). +# +# usage: bash tests/wave2/test-model-scope.sh [path-to-model-scope.py] +# +# Field (mendixlabs/mxcli#1596; mxcli v0.27.0, Mendix 10.24.18, MPR v2): one `alter entity ... +# rename attribute` rewrote 135 units in 20 modules that never referenced the attribute; the only +# change in each was the line breaks between bracketed conditions in XPath constraint strings. +# +# G the golden capture (fixtures/model-scope/golden-v0.27.0/, CAPTURE.md): a REAL mxcli v0.27.0 +# rename on a public model. Report: 1 real (the domain model), 2 noise in 2 modules [guard]; +# restore: the noise is byte-equal to before, the index matches, the real change stays. +# A the whitespace rule (literals, both quote kinds, escapes) +# B classification of the cases the capture lacks: whitespace inside a literal = real [guard], +# XPath whitespace + another field = real [guard], key order = real, added / deleted = real, +# a page data source's 'XPathConstraint', the module of a unit inside a folder +# C restore: second run writes nothing, --against REF, a model under app/, clean tree, +# a checkout that stops part-way leaves the index matching [guard] +# D check --staged: exit codes, the restore command, MODEL_SCOPE_OK=1, no model yet +# +# B-D run on SYNTHETIC models (fixtures/model-scope/mkmodel.py), shaped like the capture. +# The ContentsHash check is the fixtures' own code (mkmodel.py hashes), never the subject's. +set -uo pipefail + +TK="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SUBJECT="${1:-$TK/project-bin/model-scope.py}" +case "$SUBJECT" in /*) ;; *) SUBJECT="$PWD/$SUBJECT" ;; esac +FIX="$TK/tests/wave2/fixtures/model-scope" +. "$TK/bin/lib/portable.sh"; require_py; PYB="$PY" +WORK="$(cd "$(mktemp -d "${TMPDIR:-/tmp}/model-scope.XXXXXX")" && pwd -P)" || exit 2 +trap 'rm -rf "$WORK"' EXIT +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | sed 's/^/ /'; return 0; } +echo "== subject: $SUBJECT" +# The subject imports mxpatch.py and mxunit_bson.py from its own directory. +[ -f "$(dirname "$SUBJECT")/mxpatch.py" ] || { echo "no mxpatch.py beside $SUBJECT"; exit 2; } + +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null +gitq() { git -c user.email=t@t -c user.name=t "$@"; } +mkm() { "$PYB" -I "$FIX/mkmodel.py" "$@"; } +ms() { ( cd "$R" && "$PYB" -I "$SUBJECT" "$@" ); } +mk() { R="$WORK/$1"; shift; mkm init "$R" "$@"; } +chg() { ( cd "$R" && git status --porcelain -uall | grep -c 'mxunit' ); } +uf() { "$PYB" -I -c " +import sys,uuid +ns=uuid.UUID('6f1c1c1e-0000-4000-8000-000000000001'); u=str(uuid.uuid5(ns,sys.argv[1])) +print('mprcontents/%s/%s/%s.mxunit'%(u[:2],u[2:4],u))" "$1"; } +sql2mpr() { "$PYB" -I -c "import sqlite3,sys;db=sqlite3.connect(sys.argv[2]);db.executescript(open(sys.argv[1]).read());db.commit()" "$1" "$2"; } + +echo "== G: golden capture, real mxcli v0.27.0 rename" +GD="$FIX/golden-v0.27.0"; R="$WORK/g"; mkdir -p "$R" +cp -R "$GD/before/mprcontents" "$R/"; sql2mpr "$GD/before/TestApp.mpr.sql" "$R/TestApp.mpr" +( cd "$R" && git init -q . && gitq add -A && gitq commit -qm before ) +cp -R "$GD/after/mprcontents" "$R/"; rm "$R/TestApp.mpr"; sql2mpr "$GD/after/TestApp.mpr.sql" "$R/TestApp.mpr" +MF_OWN=mprcontents/c2/e9/c2e900a8-6b87-4916-ae7b-49fed7f0a939.mxunit # MyFirstModule.ACT_Item_Fetch +MF_OTHER=mprcontents/ce/32/ce321a44-1103-4ee9-875b-6d521bdd3178.mxunit # Other.ACT_Thing_Fetch +DM=mprcontents/84/77/84776610-13b2-48cb-a265-d5e984d63129.mxunit # MyFirstModule domain model +out="$(ms report -v)" +echo "$out" | grep -q 'vs HEAD: 3 unit file(s) changed, 1 real, 2 noise' && ok "captured rename: 1 real, 2 noise [guard]" || bad "golden counts" "$out" +echo "$out" | grep -q 'real changes by module: MyFirstModule 1' && ok "the real change is the renamed entity's module" || bad "golden module line" "$out" +echo "$out" | grep -qE "noise +M Other +$MF_OTHER" && ok "the unrelated module's microflow is noise" || bad "golden Other" "$out" +echo "$out" | grep -qE "real +M MyFirstModule +$DM .*Attributes/3/Name" && ok "the domain model's real change is the attribute Name" || bad "golden DM" "$out" +out="$(ms restore)"; rc=$? +[ "$rc" -eq 0 ] && echo "$out" | grep -q 'restored 2 noise unit(s) from HEAD' && ok "restore exits 0 and says what it restored" || bad "golden restore rc=$rc" "$out" +( cd "$R" && git diff --quiet HEAD -- "$MF_OWN" "$MF_OTHER" ) && ok "both noise units byte-equal to before" || bad "golden noise not restored" +( cd "$R" && git diff --quiet HEAD -- "$DM" ) && bad "golden: the real change was reverted" || ok "the real change is still there" +"$PYB" -I "$(dirname "$SUBJECT")/mxpatch.py" verify "$R/TestApp.mpr" 2>&1 | grep -q MISMATCH \ + && bad "golden: index out of step after restore" "$("$PYB" -I "$(dirname "$SUBJECT")/mxpatch.py" verify "$R/TestApp.mpr" 2>&1 | grep MISMATCH)" \ + || ok "no ContentsHash MISMATCH after restore (the capture's real fix hashes: golden-v0.27.0/fix-hashes-after-restore-v0.27.0.out)" + +echo "== A: whitespace outside literals only" +cat > "$WORK/strip-check.py" <<'PY' +import importlib.util, sys +spec = importlib.util.spec_from_file_location("ms", sys.argv[1]); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) +f = m.strip_ws_outside_literals +cases = [ + ("[a = 'x y']\n[b = 1]\n", "[a='x y'][b=1]"), + ('[a = "x y"] [b]', '[a="x y"][b]'), + ("[n = 'it\\'s a b'] [c]", "[n='it\\'s a b'][c]"), + ("[a = '\n']", "[a='\n']"), + ("[a = 'open [b]", "[a='open [b]"), + ("[a = \"it's\"] [b]", "[a=\"it's\"][b]"), +] +bad = [(i, f(i), w) for i, w in cases if f(i) != w] +print("OK" if not bad else bad) +PY +out="$("$PYB" -I "$WORK/strip-check.py" "$SUBJECT")" +[ "$out" = OK ] && ok "strip: literals kept, escapes and nested quotes honoured" || bad "strip" "$out" + +echo "== B: classification" +mk b2; mkm edit "$R" MfA1 literal +ms report | grep -q '1 real, 0 noise' && ok "whitespace inside a quoted literal is real [guard]" || bad "literal not real" "$(ms report)" +mk b3; mkm edit "$R" MfB2 wsfield +out="$(ms report -v)" +echo "$out" | grep -q '1 real, 0 noise' && echo "$out" | grep -q 'field changed: /Caption' && ok "XPath whitespace + another field = real [guard]" || bad "mixed not real" "$out" +mk b4; mkm edit "$R" MfB1 field +ms report | grep -q '1 real, 0 noise' && ok "another field alone = real" || bad "field not real" +mk b5; mkm edit "$R" MfB1 reorder +ms report | grep -q '1 real, 0 noise' && ok "key order change = real" || bad "reorder not real" +mk b6 +mkm delete "$R" MfA2; mkm add "$R" MfNew ModuleB; mkm edit "$R" MfB1 ws; mkm edit "$R" MfB2 field; mkm edit "$R" PgA ws; mkm edit "$R" MfA1 ws +out="$(ms report -v)" +echo "$out" | grep -q '6 unit file(s) changed, 3 real, 3 noise' && ok "counts: 3 real (added, deleted, field), 3 noise" || bad "counts" "$out" +echo "$out" | grep -q 'real changes by module: ModuleB 2, ModuleA 1' && ok "real changes per module (added in B, deleted from A)" || bad "per-module line" "$out" +echo "$out" | grep -qE 'real +D ModuleA ' && ok "a deleted unit's module comes from the ref's .mpr" || bad "deleted module" "$out" +echo "$out" | grep -qE "noise +M ModuleA +$(uf PgA)" && ok "a page data source's 'XPathConstraint' counts" || bad "page key" "$out" +echo "$out" | grep -qE "noise +M ModuleA +$(uf MfA1)" && ok "a unit inside a folder resolves to its module" || bad "folder walk" "$out" +echo "$out" | grep -q 'noise: 3 unit(s) in 2 module(s)' && ok "noise line counts modules" || bad "noise line" "$out" + +echo "== C: restore" +out="$(ms restore)"; rc=$? +[ "$rc" -eq 0 ] && echo "$out" | grep -q 'restored 3 noise unit(s)' && ok "restore exits 0" || bad "restore rc=$rc" "$out" +for n in MfB1 PgA MfA1; do + ( cd "$R" && git diff --quiet HEAD -- "$(uf $n)" ) || { bad "$n not byte-equal to HEAD"; break; } +done && ok "noise files are byte-equal to HEAD" +[ "$(mkm hashes "$R")" = clean ] && ok "ContentsHash index matches the files" || bad "index not clean: $(mkm hashes "$R")" +( cd "$R" && git diff --quiet HEAD -- "$(uf MfB2)" ) && bad "the real change was reverted" || ok "the real change is still there" +( cd "$R" && test -e "$(uf MfNew)" ) && ok "the added unit is untouched" || bad "the added unit vanished" +st1="$(cd "$R" && git status --porcelain -uall; shasum "$R/App.mpr")" +out="$(ms restore)"; rc=$? +st2="$(cd "$R" && git status --porcelain -uall; shasum "$R/App.mpr")" +[ "$st1" = "$st2" ] && [ "$rc" -eq 0 ] && ok "second run writes nothing" || bad "second run changed things" "$out" + +mk c2 +mkm edit "$R" MfB1 field; ( cd "$R" && gitq add -A && gitq commit -qm second ) +mkm edit "$R" MfB2 ws +out="$(ms report --against HEAD~1)" +echo "$out" | grep -q 'vs HEAD~1: 2 unit file(s) changed, 1 real, 1 noise' && ok "--against REF compares with that commit" || bad "--against" "$out" +ms restore --against HEAD~1 >/dev/null +( cd "$R" && git diff --quiet HEAD~1 -- "$(uf MfB2)" ) && ok "--against: noise restored from REF" || bad "--against restore" + +mk c3 --sub app +mkm edit "$R" MfB1 ws; mkm edit "$R" MfB2 field +out="$(ms restore)"; rc=$? +echo "$out" | grep -q 'restored 1 noise unit(s)' && [ "$(chg)" -eq 1 ] && [ "$(mkm hashes "$R")" = clean ] \ + && ok "model under app/ (run from the project root)" || bad "two-tree rc=$rc" "$out" + +mk c4 +out="$(ms restore)"; rc=$? +[ "$rc" -eq 0 ] && echo "$out" | grep -q '0 unit file(s) changed' && ok "nothing changed: exit 0" || bad "clean tree" "$out" +out="$(ms report --against nosuchref 2>&1)"; rc=$? +[ "$rc" -eq 2 ] && ok "unknown ref: exit 2" || bad "bad ref rc=$rc" "$out" +# A checkout that stops part-way (one unit's directory not writable): the unit that did come +# back gets its ContentsHash row, the other stays as the write left it, and the index matches. +mk c9 +mkm edit "$R" MfB1 ws; mkm edit "$R" PgA ws +LOCK="$R/$(dirname "$(uf PgA)")"; chmod 0555 "$LOCK" +out="$(ms restore 2>&1)"; rc=$? +chmod 0755 "$LOCK" +[ "$rc" -eq 2 ] && echo "$out" | grep -q 'stopped part-way' && [ "$(chg)" -eq 1 ] && [ "$(mkm hashes "$R")" = clean ] \ + && ok "partial restore: exit 2, restored unit re-indexed, index matches [guard]" || bad "partial restore rc=$rc chg=$(chg) $(mkm hashes "$R")" "$out" + +echo "== D: check --staged" +mk d1 +mkm edit "$R" MfB1 ws; ( cd "$R" && git add -A ) +out="$(ms check --staged 2>&1)"; rc=$? +[ "$rc" -eq 1 ] && ok "staged noise: exit 1 [guard]" || bad "staged noise rc=$rc" "$out" +echo "$out" | grep -q ' -I bin/model-scope.py restore' && echo "$out" | grep -q 'ModuleB' && ok "names the module and the restore command" || bad "message" "$out" +out="$(MODEL_SCOPE_OK=1 ms check --staged 2>&1)"; rc=$? +[ "$rc" -eq 0 ] && echo "$out" | grep -q 'MODEL_SCOPE_OK=1: committing' && ok "MODEL_SCOPE_OK=1 lets it through, out loud" || bad "override rc=$rc" "$out" +ms restore >/dev/null +ms check --staged >/dev/null 2>&1 && ok "after restore the staged index is clean (exit 0)" || bad "still refused after restore" +mk d2 +mkm edit "$R" MfB2 field; ( cd "$R" && git add -A ) +ms check --staged >/dev/null 2>&1 && ok "staged real change: exit 0" || bad "real change refused" +mk d3 +mkm edit "$R" MfB1 ws # not staged +ms check --staged >/dev/null 2>&1 && ok "noise only in the working tree: exit 0 (the index is what counts)" || bad "unstaged noise refused" +mkdir -p "$WORK/d4" && R="$WORK/d4" && ( cd "$R" && git init -q . && echo hi > f && git add f ) +ms check --staged >/dev/null 2>&1 && ok "no model in the repo yet: exit 0" || bad "pre-model repo refused" +mk d5 +mkm edit "$R" MfB1 ws; mkm edit "$R" MfA1 ws; mkm edit "$R" PgA ws; ( cd "$R" && git add -A ) +out="$(ms check --staged 2>&1)" +echo "$out" | grep -q 'refused: 3 staged unit file' && ok "counts every staged noise unit" || bad "count" "$out" + +echo "" +echo "PASS: $PASS FAIL: $FAIL" +[ "$FAIL" -eq 0 ] From 9f3561eca7506a21ac3875051d258e00040001ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 16:04:22 +0000 Subject: [PATCH 2/2] docs(model-scope): docstring says nothing calls restore yet Independent review: the docstring said exec.sh runs it after every write; it is not wired in (the CHANGELOG line says so). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- project-bin/model-scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project-bin/model-scope.py b/project-bin/model-scope.py index 77cd82f4..8c2e5728 100755 --- a/project-bin/model-scope.py +++ b/project-bin/model-scope.py @@ -25,7 +25,7 @@ every unit. restore report, then put each noise unit back from REF (`git checkout REF -- `) and set its ContentsHash row in the .mpr from the restored file (mxpatch.py's rehash), so - the index Studio Pro reads stays in step. bin/exec.sh runs this after every write. + the index Studio Pro reads stays in step. Run it by hand; nothing calls it yet. check --staged: the staged index against HEAD, for the pre-commit hook. Exit 1 when a noise unit is staged, printing the restore command. MODEL_SCOPE_OK=1 lets that one commit through with a warning.