From 1079323a462f6ff857a52708121882819b447c78 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 07:58:15 +0000 Subject: [PATCH 1/2] decompose: cut-ledger (split from #293, 2/6) bin/cut-ledger.sh + bin/lib/cut-ledger.py copied from #293's head; header no longer cites files not on master and carries the field run's real counts. New fixture tests/wave2/test-cut-ledger.sh (38 checks) on a captured line subset of #293's NativeNote slice run (template app): reproduces that run's own 57 ledger rows. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + bin/cut-ledger.sh | 75 ++++++ bin/lib/cut-ledger.py | 218 ++++++++++++++++++ tests/wave2/fixtures/cut-ledger/CAPTURE.txt | 25 ++ .../fixtures/cut-ledger/crossing-refs.tsv | 44 ++++ tests/wave2/fixtures/cut-ledger/cut-extra.tsv | 14 ++ tests/wave2/fixtures/cut-ledger/cut-rules.tsv | 48 ++++ .../fixtures/cut-ledger/expected-ledger.tsv | 58 +++++ .../fixtures/cut-ledger/exporter-prefix.txt | 56 +++++ tests/wave2/fixtures/cut-ledger/exporter.json | 48 ++++ tests/wave2/fixtures/cut-ledger/mx-check.txt | 10 + tests/wave2/fixtures/cut-ledger/slice.txt | 30 +++ tests/wave2/test-cut-ledger.sh | 138 +++++++++++ 13 files changed, 765 insertions(+) create mode 100755 bin/cut-ledger.sh create mode 100644 bin/lib/cut-ledger.py create mode 100644 tests/wave2/fixtures/cut-ledger/CAPTURE.txt create mode 100644 tests/wave2/fixtures/cut-ledger/crossing-refs.tsv create mode 100644 tests/wave2/fixtures/cut-ledger/cut-extra.tsv create mode 100644 tests/wave2/fixtures/cut-ledger/cut-rules.tsv create mode 100644 tests/wave2/fixtures/cut-ledger/expected-ledger.tsv create mode 100644 tests/wave2/fixtures/cut-ledger/exporter-prefix.txt create mode 100644 tests/wave2/fixtures/cut-ledger/exporter.json create mode 100644 tests/wave2/fixtures/cut-ledger/mx-check.txt create mode 100644 tests/wave2/fixtures/cut-ledger/slice.txt create mode 100644 tests/wave2/test-cut-ledger.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 59bca1b3..bb271fb4 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(cut-ledger): **`bin/cut-ledger.sh` writes the cut ledger of one decompose slice — one row per crossing edge with its resolution (resolved / stub / handoff / retire / platform) and evidence — and exits 1 while any edge has none** (split from [#293](https://github.com/MendixMau/mxcli-project-toolkit/pull/293), 2/6). Reads the source app's catalog `refs` table (or a `--refs` TSV), a slice file (modules and single documents), first-match rules with mandatory evidence, `mx check` CE1613 names the catalog never recorded (check-only rows), the module exporter's JSON (modules it counts with no ledger row are printed), and verbatim `--extra` rows. #293 shipped it with no fixture; new `tests/wave2/test-cut-ledger.sh` (38 checks) runs on a captured line subset of #293's field run (43 of 346 crossing refs, one key per rule) and reproduces that run's own 57 ledger rows column for column, the pre-fix exporter note, UNRESOLVED → exit 1, refusals → exit 2, and `--catalog` at single-tree and `app/` paths. Header corrected to the run's real counts (346 refs → 308 rows; it said 199). — MendixMau (#293 field run on a public template app) - process(docs): **1.0 release notes, a versioning rule, and a README for adopters.** New `docs/releases/v1.0.md`: the first named milestone, covering 2026-09-30 to 2026-10-14 by theme (independent review before merge, baseline words 79,997 → 76,259, exec.sh lock and restart verdicts, lint batches 1–4 with SEC014 and the stock-revert guard, journey/e2e fixes, the Stage 3 interaction model, delivery rounds), with upgrade steps, known gaps and credits. `docs/RELEASE-PROCESS.md` §8 is the single source for versioning: a milestone (1.0, 1.1, 2.0) is a train release the maintainer also names, with curated notes, a GitHub Release and a CHANGELOG pointer; major = a consuming project breaks without action (needs a migration section and a sync path), minor = no action needed; deprecations are announced in one milestone and removed no earlier than the next major; the `vYYYY.MM.DD` train and `bin/cut-release.sh` are unchanged; plus how a consumer checks, upgrades and pins a version. The header above now defers to it. README rewritten for newcomers: what it is, how it works, ways to use it, quick start, Make it yours (company brain, CLAUDE.local.md, lint opt-outs, waivers and --adopt, interview mode, exec approval and permission profiles, agent stubs, other harnesses), testing, a call for input, releases. The earlier material sits under Reference with the generated routing blocks untouched. Corrected: the Windows `pgrep` hazard (replaced by the per-project exec lock in #304), the hand-edit steps in "How to add a new skill" (tables are generated), and the stale Stage 6 and `examples/` notes. — MendixMau - fix(interview-mode): **`attended` and `unattended` are now read as aliases for `steering` and `auto` — a project that opted into unattended as documented was silently run as steering.** CLAUDE.md, the runbook and intake Q9 taught `Interview mode: unattended`, but `bin/interview-mode.sh` knew only `steering | assist | auto` and fell back to steering. The aliases are accepted in every source (env, `.interview-mode`, `PROJECT.md`, `--set`) under the same parse rules, with one stderr note. The `PROJECT.md` value is read after stripping bold/backticks: a bare mode or alias (`**auto**`, `` `auto` ``, `auto.`) resolves, a note may follow `steering`, but `assist`/`auto` with any trailing text falls back to steering with a "must stand alone on the line" message — so an annotated or hedged line never resolves quieter than before (two hedge word lists were tried in review and both leaked); env, session file and `--set` stay exact. Output stays canonical so `interview-gate.sh` is unchanged, and any other value still falls back to steering. Docs now name the canonical values (vocabulary in `interview-protocol.md`); intake Q9's body offers the three modes with steering the default (heading kept: it is sync's locator), and `sync-project.sh` reads an earlier unanswered Q9 body as unanswered rather than "edited". `test-interview-mode.sh` +57 checks (19 → 76) — MendixMau (found in the 1.0 README review) - fix(dispatching, sync-project, obligation-check): **review follow-ups for the 2026-10-14 integration batch.** `dispatching.md` names app-restart exits 4 and 5 beside 3 and 1. `test-test-scratch.sh` drops `ls | grep -q` under pipefail for here-strings; sync step 4d no longer aborts on empty doctor output; `_ob_has_denominator` strips NUL bytes so bash stops warning. — integration 2026-10-14 reviews diff --git a/bin/cut-ledger.sh b/bin/cut-ledger.sh new file mode 100755 index 00000000..580c4a95 --- /dev/null +++ b/bin/cut-ledger.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# cut-ledger.sh: the cut ledger of one Decompose slice — one row per crossing edge, each with a +# resolution and its evidence, or the run fails. +# +# usage: bin/cut-ledger.sh (--catalog | --refs ) --slice +# --rules [--check-log ]... +# [--exporter-json ]... [--extra ]... +# [--dump-refs ] -o +# +# WHAT THIS IS. The Stage 5 slice-loop instrument of the decompose-and-rehost plan (step "replace each +# crossing edge"; the plan is split out of #293 and lands separately). bin/cut-score.sh scores a +# proposed cut at module level; +# this lists the edges of the cut you are actually executing, at document level, and refuses to +# finish while any of them has no decision. "Done" for a slice is not "mx check is green" — a stub +# turns an edge green and carries none of its behaviour — it is "every edge has a row that says +# resolved, stub, handoff or retire, with the evidence". +# +# WHAT IT READS. +# --catalog the mxcli catalog of the SOURCE app (.mxcli/catalog.db beside the .mpr, written by +# `mxcli -p .mpr graph-report`, which project-bin/app-facts.sh runs as the +# mandatory Stage 0 facts step), refs table only. Or --refs: a TSV with the header +# SourceType SourceName TargetType TargetName RefKind (what --dump-refs writes; the +# committed fixture form, since a catalog is tens of MB). +# --slice one name per line: a module (moves whole) or Module.Doc (a document that moves with +# the slice although its module stays, e.g. the Synchronization flows of one entity). +# --rules tab-separated: direction (out|in|check|*), pattern (fnmatch on the far-side +# document, or 'OwnSide -> FarSide'), resolution, evidence. First match wins. +# Resolutions: resolved[:how] · stub · handoff · retire · platform. A row with no +# matching rule is UNRESOLVED and the run exits 1. A rule with no evidence is refused. +# --check-log mx check output on the slice package (the exporter's self-check, Mendix 9.24 `mx check` +# on the package's project.mpr). Every CE1613 name it reports that the catalog did not +# record becomes a `check-only` row — the catalog does not see widget actions (App +# events), layout placeholders, or images. +# --exporter-json the module-MPK exporter's --json output (export-module-mpk.py, split out of #293 +# and landing separately; any JSON with crossing_refs_by_module works): each module it counts +# that has no out-edge row is printed as `exporter-only` (a false positive or a +# catalog blind spot — read which). +# --extra rows no catalog holds, verbatim (direction, kind, source_doc, target_doc, refs, +# resolution, evidence): second-order edges of a module brought along to satisfy an +# edge, app-level settings the package scrubs, Java hand-off edits. +# +# WHAT IT WRITES. : edge, direction, kind, source_doc, target_doc, refs, resolution, +# evidence. Out = the slice uses something that stays behind; in = something that stays behind uses +# the slice (the monolith-side work of the strangler). System is auto-resolved as platform. +# +# WHAT IT CANNOT SEE. Whether the resolution works: that is the target's mx check and mxbuild, and +# for a stub it never is — the evidence column says what the stub does not carry. Java and +# JavaScript bodies naming another module, and references by string (deep links, REST paths). +# +# Exit: 0 every row has a resolution · 1 at least one UNRESOLVED row · 2 bad arguments +# +# Field run: public field-service template app 9.24.2 -> its NativeNote slice into Mendix 11.13.0 +# (2026-10-12): 346 crossing refs -> 308 ledger rows, 0 UNRESOLVED, 0.15 s. A captured subset of that +# run's inputs and ledger is the fixture: tests/wave2/fixtures/cut-ledger/ (test-cut-ledger.sh). +# +# LAYOUT. Every input is an explicit path (no .mpr discovery), so single-tree vs two-tree (`app/`) +# only changes the --catalog argument: /.mxcli/catalog.db or /app/.mxcli/catalog.db. +# +# portability-ok: bash 3.2, Python 3 resolved through portable.sh + +set -u + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +. "$HERE/lib/portable.sh" +require_py + +case "${1:-}" in + -h|--help|"") sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'; [ -z "${1:-}" ] && exit 2; exit 0 ;; +esac + +"$PY" -I "$HERE/lib/cut-ledger.py" "$@" +rc=$? +[ "$rc" -gt 1 ] && exit 2 +exit "$rc" diff --git a/bin/lib/cut-ledger.py b/bin/lib/cut-ledger.py new file mode 100644 index 00000000..9155dd98 --- /dev/null +++ b/bin/lib/cut-ledger.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +"""cut-ledger.py — engine of bin/cut-ledger.sh (read that header; this file has no CLI of its own +beyond what the wrapper passes). Python 3 stdlib only.""" +import argparse +import fnmatch +import json +import os +import re +import sqlite3 +import sys + +COLS = ["edge", "direction", "kind", "source_doc", "target_doc", "refs", "resolution", "evidence"] +RESOLUTIONS = ("resolved", "stub", "handoff", "retire", "platform") +REFS_COLS = ["SourceType", "SourceName", "TargetType", "TargetName", "RefKind"] + + +def die(msg): + print(msg, file=sys.stderr) + sys.exit(2) + + +def doc_of(name): + """Qualified document name: the first two dotted parts (Module.Doc[.Member…] -> Module.Doc).""" + p = name.split(".") + return ".".join(p[:2]) if len(p) >= 2 else name + + +def read_slice(path): + mods, docs = set(), set() + for line in open(path, encoding="utf-8"): + line = line.split("#", 1)[0].strip() + if not line: + continue + (docs if "." in line else mods).add(line) + if not mods and not docs: + die("cut-ledger: slice file %s names nothing" % path) + return mods, docs + + +def in_slice(name, mods, docs): + return name.split(".")[0] in mods or doc_of(name) in docs + + +def read_refs(args): + rows = [] + if args.catalog: + db = sqlite3.connect("file:%s?mode=ro" % args.catalog, uri=True) + if not db.execute("select 1 from sqlite_master where name='refs'").fetchone(): + die("cut-ledger: %s has no refs table (not an mxcli catalog; run graph-report)" % args.catalog) + for r in db.execute("select %s from refs" % ", ".join(REFS_COLS)): + rows.append(dict(zip(REFS_COLS, r))) + else: + with open(args.refs, encoding="utf-8") as f: + head = f.readline().rstrip("\n").split("\t") + if head != REFS_COLS: + die("cut-ledger: %s header must be %s" % (args.refs, "\t".join(REFS_COLS))) + for line in f: + v = line.rstrip("\n").split("\t") + if len(v) == len(REFS_COLS): + rows.append(dict(zip(REFS_COLS, v))) + return rows + + +def read_rules(path): + rules = [] + for n, line in enumerate(open(path, encoding="utf-8"), 1): + if not line.strip() or line.lstrip().startswith("#"): + continue + v = line.rstrip("\n").split("\t") + if len(v) < 4: + die("cut-ledger: %s:%d needs 4 tab-separated fields (direction, pattern, resolution, evidence)" % (path, n)) + d, pat, res, ev = v[0].strip(), v[1].strip(), v[2].strip(), v[3].strip() + if d not in ("out", "in", "check", "*"): + die("cut-ledger: %s:%d direction %r is not out|in|check|*" % (path, n, d)) + if res.split(":")[0] not in RESOLUTIONS: + die("cut-ledger: %s:%d resolution %r is not one of %s (a ':detail' suffix is allowed)" % (path, n, res, "|".join(RESOLUTIONS))) + if not ev: + die("cut-ledger: %s:%d has no evidence; a resolution without evidence is a claim" % (path, n)) + rules.append((d, pat, res, ev, n)) + return rules + + +def match(rules, direction, other_doc, own_doc): + """First rule whose direction fits and whose pattern matches the far-side doc. + A pattern 'A -> B' matches own side A and far side B (both fnmatch).""" + for d, pat, res, ev, n in rules: + if d not in ("*", direction): + continue + if "->" in pat: + a, b = [x.strip() for x in pat.split("->", 1)] + if fnmatch.fnmatchcase(own_doc, a) and fnmatch.fnmatchcase(other_doc, b): + return res, ev, n + elif fnmatch.fnmatchcase(other_doc, pat): + return res, ev, n + return None + + +CHECK_NAME = re.compile(r"\[error\] \[CE1613\] \"The selected [^']*'([A-Za-z0-9_]+\.[^']+)' no longer exists") + + +def main(): + ap = argparse.ArgumentParser(prog="cut-ledger") + src = ap.add_mutually_exclusive_group(required=True) + src.add_argument("--catalog") + src.add_argument("--refs") + ap.add_argument("--slice", required=True) + ap.add_argument("--rules", required=True) + ap.add_argument("--check-log", action="append", default=[]) + ap.add_argument("--exporter-json", action="append", default=[]) + ap.add_argument("--extra", action="append", default=[]) + ap.add_argument("--dump-refs") + ap.add_argument("-o", "--out", required=True) + args = ap.parse_args() + + mods, docs = read_slice(args.slice) + refs = read_refs(args) + rules = read_rules(args.rules) + + crossing = [r for r in refs if in_slice(r["SourceName"], mods, docs) != in_slice(r["TargetName"], mods, docs)] + if args.dump_refs: + with open(args.dump_refs, "w", encoding="utf-8") as f: + f.write("\t".join(REFS_COLS) + "\n") + for r in sorted(crossing, key=lambda r: (r["SourceName"], r["TargetName"], r["RefKind"])): + f.write("\t".join(r[c] for c in REFS_COLS) + "\n") + + # one ledger row per (direction, kind, source doc, target doc); refs = how many catalog refs it carries + agg = {} + for r in crossing: + direction = "out" if in_slice(r["SourceName"], mods, docs) else "in" + key = (direction, r["RefKind"], doc_of(r["SourceName"]), doc_of(r["TargetName"])) + agg[key] = agg.get(key, 0) + 1 + + rows, used_rules = [], set() + for (direction, kind, s, t), n in sorted(agg.items(), key=lambda kv: (kv[0][0] != "out", kv[0][3], kv[0][2], kv[0][1])): + own, other = (s, t) if direction == "out" else (t, s) + if other.split(".")[0] == "System" and direction == "out": + res, ev = "platform", "System module exists in every Mendix app" + else: + m = match(rules, direction, other, own) + if m: + res, ev, rn = m + used_rules.add(rn) + else: + res, ev = "UNRESOLVED", "no rule in %s matches %s" % (os.path.basename(args.rules), other) + rows.append([direction, kind, s, t, str(n), res, ev]) + + # mx check names the catalog never recorded (widget actions, layout placeholders, images, ...) + covered = {r[3] for r in rows if r[0] == "out"} | {doc_of(r[3]) for r in rows if r[0] == "out"} + check_extra = {} + for log in args.check_log: + for line in open(log, encoding="utf-8", errors="replace"): + m = CHECK_NAME.search(line) + if not m: + continue + name = m.group(1) + if in_slice(name, mods, docs) or doc_of(name) in covered: + continue + check_extra[doc_of(name)] = check_extra.get(doc_of(name), 0) + 1 + for d, n in sorted(check_extra.items()): + m = match(rules, "check", d, "") or match(rules, "out", d, "") + if m: + res, ev, rn = m + used_rules.add(rn) + else: + res, ev = "UNRESOLVED", "named by mx check, not in the catalog, no rule matches" + rows.append(["out", "check-only", "(mx check)", d, str(n), res, ev]) + + # edges no catalog holds (second-order edges of a module brought along, app-level settings, + # Java hand-offs): direction, kind, source_doc, target_doc, refs, resolution, evidence — verbatim + for ex in args.extra: + for n, line in enumerate(open(ex, encoding="utf-8"), 1): + if not line.strip() or line.lstrip().startswith("#"): + continue + v = line.rstrip("\n").split("\t") + if len(v) != 7 or v[5].split(":")[0] not in RESOLUTIONS or not v[6].strip(): + die("cut-ledger: %s:%d needs 7 fields with a known resolution and evidence" % (ex, n)) + rows.append(v) + + # reconcile against the exporter's per-module counts: a module it names with no ledger row is reported + exporter_notes = [] + out_mods = {} + for r in rows: + if r[0] == "out": + out_mods[r[3].split(".")[0]] = out_mods.get(r[3].split(".")[0], 0) + 1 + for ej in args.exporter_json: + txt = open(ej, encoding="utf-8").read() + j = json.loads(txt[txt.index("{"):txt.rindex("}") + 1]) + for mod, units in sorted(j.get("crossing_refs_by_module", {}).items()): + if mod not in out_mods: + exporter_notes.append("%s: exporter counts %d unit(s) naming it, ledger has 0 out-edges to it" % (mod, units)) + + with open(args.out, "w", encoding="utf-8") as f: + f.write("\t".join(COLS) + "\n") + for i, r in enumerate(rows, 1): + f.write("\t".join(["E%03d" % i] + r) + "\n") + + tot = {} + for r in rows: + k = r[5].split(":")[0] + tot[k] = tot.get(k, 0) + 1 + unused = [n for d, p, res, ev, n in rules if n not in used_rules] + n_extra = len(rows) - len(agg) - len(check_extra) + print("cut-ledger: %d crossing refs -> %d ledger rows (%d out, %d in from the catalog; %d check-only; %d extra) -> %s" % ( + len(crossing), len(rows), sum(1 for k in agg if k[0] == "out"), sum(1 for k in agg if k[0] == "in"), + len(check_extra), n_extra, args.out)) + print(" by resolution: " + ", ".join("%s %d" % (k, tot[k]) for k in sorted(tot))) + for note in exporter_notes: + print(" exporter-only: " + note) + if unused: + print(" rules never used (lines): " + ", ".join(map(str, unused))) + if tot.get("UNRESOLVED"): + print(" UNRESOLVED rows: %d — every crossing edge needs a rule (resolved, stub, handoff, retire)" % tot["UNRESOLVED"]) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/wave2/fixtures/cut-ledger/CAPTURE.txt b/tests/wave2/fixtures/cut-ledger/CAPTURE.txt new file mode 100644 index 00000000..c8d3d38b --- /dev/null +++ b/tests/wave2/fixtures/cut-ledger/CAPTURE.txt @@ -0,0 +1,25 @@ +Captured 2026-10-12 in #293 (split from it): the NativeNote slice of the public field-service +template app 9.24.2 (Mendix Marketplace) moved into a fresh Mendix 11.13.0 app, mxcli v0.26.0, +Linux. The full run read 346 crossing refs and wrote 308 ledger rows, 0 UNRESOLVED, exit 0. + +Nothing here is hand-written. Every file is verbatim from that run, or a verbatim line subset of it: + + slice.txt verbatim (NativeNote + 26 Synchronization documents) + cut-rules.tsv verbatim (the field run's decisions, 43 rules) + cut-extra.tsv verbatim (13 rows no catalog holds) + exporter.json verbatim exporter --json after its name-boundary fix (the one fed to the run) + exporter-prefix.txt verbatim transcript of the exporter BEFORE that fix (counts Note x54, Technician x2) + crossing-refs.tsv SUBSET of the run's --dump-refs output (346 refs): every ref of one ledger key + per distinct evidence string, so each rule is exercised once and System once + -> 43 refs, 42 keys. Lines copied unchanged, order kept. + mx-check.txt SUBSET of the mx 9.24 check on the slice package (186 CE1613 lines): the first 3 + (names the catalog already has) + every line naming the two catalog-blind docs + expected-ledger.tsv SUBSET of the run's cut-ledger.tsv: the 57 rows this input must reproduce, with + their original E-numbers (the test compares columns 2-8 and renumbers) + +Known right answer (bin/cut-ledger.sh with all of the above): + 43 crossing refs -> 57 ledger rows (20 out, 22 in from the catalog; 2 check-only; 13 extra) + by resolution: handoff 20, platform 1, resolved 24, retire 1, stub 11 + no rule unused; no exporter-only note with exporter.json; exactly one with exporter-prefix.txt + ("Technician: exporter counts 2 unit(s) naming it, ledger has 0 out-edges to it", as in the field + run's transcript) diff --git a/tests/wave2/fixtures/cut-ledger/crossing-refs.tsv b/tests/wave2/fixtures/cut-ledger/crossing-refs.tsv new file mode 100644 index 00000000..a792aa53 --- /dev/null +++ b/tests/wave2/fixtures/cut-ledger/crossing-refs.tsv @@ -0,0 +1,44 @@ +SourceType SourceName TargetType TargetName RefKind +MICROFLOW Form.InstanceForm_PrepareDelete_InstanceItems ENTITY Synchronization.OfflineAction parameter +MICROFLOW General.TurnSoftDeleteToHard MICROFLOW NativeNote.NativeImage_TurnSoftDeleteToHard call +MICROFLOW MasterData.ACT_NativeNote_Save_MxAdmin ENTITY NativeNote.NativeNote parameter +NANOFLOW NativeMobile.DS_GetObjectCounts ENTITY Synchronization.NoteImage retrieve +SNIPPET NativeMobile.Snippet_NativeNoteList NANOFLOW NativeNote.ACT_NativeNote_Create action +MICROFLOW NativeMobileRelated.NativeNoteNP_GetTreeData ENTITY NativeNote.NativeImage retrieve +MICROFLOW NativeModelCheck.DeleteNativeShadowModel ENTITY NativeNote.NativeImage retrieve +NANOFLOW NativeNote.ACT_NativeNote_Delete ENUMERATION_VALUE NativeMobile.ENUM_LoadingType.DeletingNote value +NANOFLOW NativeNote.ACT_NativeNote_Delete NANOFLOW NativeMobile.LoadHelper_StartLoading call +NANOFLOW NativeNote.ACT_NativeNote_Delete NANOFLOW NativeMobile.NativeWorkorder_AppointmentDetailsHelper_AddNoteCount call +NANOFLOW NativeNote.ACT_NativeNote_Delete NANOFLOW Synchronization.Synchronize call +ENTITY NativeNote.NativeImage ASSOCIATION Administration.Account_NativeTechnician xpath +ENTITY NativeNote.NativeImage ASSOCIATION NativeMobile._NativeWorkOrder_NativeTechnician_EditMy xpath +MICROFLOW NativeNote.NativeImage_DeleteOld JAVA_ACTION CommunityCommons.EndTransaction call +MICROFLOW NativeNote.NativeImage_DeleteOld ATTRIBUTE NativeMobile.NativeTicket.DisplayEnd xpath +MICROFLOW NativeNote.NativeImage_DeleteOld ASSOCIATION NativeMobile.NativeWorkOrder_NativeTicket xpath +PAGE NativeNote.NativeImage_NewEdit LAYOUT FSM_Specific.NativePhone_FullPage_Back layout +NANOFLOW NativeNote.NativeNote_AddImage ENUMERATION_VALUE NativeMobileResources.PictureQuality.medium value +NANOFLOW NativeNote.NativeNote_GetNativeWorkorder ENTITY NativeMobile.NativeWorkOrder return +NANOFLOW NativeNote.NativeNote_Get_OriginalValue JAVASCRIPT_ACTION NativeMobile.JS_GetOriginalValue call +ASSOCIATION NativeNote.NativeNote_NativeTechnician ENTITY NativeTechnician.NativeTechnician associate +NANOFLOW NativeNote.NativeNote_ResetAppointmentDetailsHelper ATTRIBUTE NativeMobile.AppointmentDetailsHelper.NoteCount member +NANOFLOW NativeNote.NativeNote_SetCreated NANOFLOW Utils.Account_GetCurrent_NF call +ENTITY NativeTechnician.NativeTechnician ASSOCIATION NativeNote.NativeNote_NativeTechnician xpath +NAVIGATION Navigation.NativePhone ENTITY NativeNote.NativeImage sync +NAVIGATION Navigation.NativePhone ENTITY Synchronization.NoteImage sync +MICROFLOW Note.ImageList_Delete MICROFLOW NativeNote.NativeImageList_Delete call +MICROFLOW Synchronization.NativeImageList_AddAndSynchronize ENTITY NativeNote.NativeImage parameter +NANOFLOW Synchronization.NativeInstanceItem_SetupOfflinePictureActions ENUMERATION_VALUE NativeNote.ENUM_ImageAction.Add_Image value +NANOFLOW Synchronization.NativeInstanceItem_SetupOfflinePictureActions ENUMERATION_VALUE NativeNote.ENUM_ImageAction.Delete_image value +MICROFLOW Synchronization.NoteImage_ProcessUpdatingNativeImage ENTITY NativeNote.NativeImage change +MICROFLOW Synchronization.Note_ProcessUpdatingNativeNote ENTITY NativeNote.NativeNote change +NANOFLOW Synchronization.OfflineImageAction_Create JAVASCRIPT_ACTION Note.JS_CloneFileDocument call +NANOFLOW Synchronization.OfflineImageAction_Create ATTRIBUTE System.FileDocument.Name member +MICROFLOW Synchronization.OfflineImageAction_Process ENTITY Synchronization.OfflineImageAction parameter +MICROFLOW Synchronization.OfflineImageAction_Validate ENTITY Synchronization.OfflineImageAction parameter +MICROFLOW Synchronization.OfflineNoteAction_NewNote ENTITY Synchronization.OfflineNoteAction parameter +MICROFLOW Synchronization.OfflineNoteAction_UpdateNote ENTITY Synchronization.OfflineNoteAction parameter +NANOFLOW Synchronization.Private_SynchronizeOfflineActions ENTITY Synchronization.NoteImage retrieve +RULE Synchronization.Rule_Note_HasChanged ENTITY NativeNote.NativeNote parameter +NANOFLOW Synchronization.UploadHelper_CheckAndGet NANOFLOW NativeMobile.StorageItem_Check call +NANOFLOW Synchronization.UploadHelper_Get JAVASCRIPT_ACTION NanoflowCommons.GetStorageItemObject call +NANOFLOW Synchronization.UploadHelper_Set_LastUploadStartedDate ENTITY Synchronization.UploadHelper change diff --git a/tests/wave2/fixtures/cut-ledger/cut-extra.tsv b/tests/wave2/fixtures/cut-ledger/cut-extra.tsv new file mode 100644 index 00000000..ea9c4b63 --- /dev/null +++ b/tests/wave2/fixtures/cut-ledger/cut-extra.tsv @@ -0,0 +1,14 @@ +# rows no catalog holds: direction, kind, source_doc, target_doc, refs, resolution, evidence +out second-order Utils (mpk) ValidationFeedback.* 4 resolved:marketplace Utils' own edge; marketplace install 110282 -> 3.4.0 (captures/15) +out second-order Utils (mpk) CommunityCommons.* 3 resolved:marketplace Utils' own edge; CommunityCommons 11.5.1 (captures/15) +out second-order Utils (mpk) Administration.Account 3 resolved:marketplace Utils' own edge; starter's Administration 4.5.0 has Account +out second-order Utils (mpk) NanoflowCommons.* 1 resolved:marketplace Utils' own edge; starter's NanoflowCommons 7.2.1 +out second-order Utils (mpk) NativeMobileResources.* 2 resolved:marketplace Utils' own edge; starter's NativeMobileResources 12.2.2 +out second-order Utils (mpk) AppSettings.Environment 1 stub CUT STUB constant (default 'Local', exposed to client) — edges-C-app.mdl; without it mx check CE0117 at Decision 'Production?' +out java-handoff Utils (mpk) javasource/utils/actions/GetFileType.java 1 handoff:done this.context() is a Mendix 9 API -> getContext() (captures/23); mxbuild javac error before, BUILD SUCCEEDED after +out java-handoff Utils (mpk) javasource/utils/actions/{JA_RoundMinutes,executeInBackgroundImmediately}.java 2 handoff:done orphan action files (no Java action unit in Utils) extending com.mendix.webui.CustomJavaAction — deleted (captures/23) +out java-handoff Utils (mpk) userlib/tika-core-1.28.4.jar 1 handoff:done source RequiredLib marker names former module FileTypeChecker, so the exporter did not ship it; copied with a .Utils.RequiredLib marker (captures/23) +out app-setting NativeNote pages Navigation.NativePhone (native profile) 1 resolved:starter mxcli cannot create a native profile (only the fixed web kinds) and without one every nanoflow show-page of a native page is CE6081: target is Blank Native Mobile App 11.13.0 (Marketplace 109511) via mx create-project, not mxcli new (captures/14) +out app-setting Navigation.NativePhone sync lines for NativeNote, NativeImage, Offline{Note,Image}Action, NoteImage, NativeWorkOrder, NativeTicket, NativeTechnician, Account 9 resolved:mdl the package scrubs navigation; source lines replayed for the slice's entities — edges-C-app.mdl +out widget NativeNote pages widgets/AppEvents.mpk, Repeater.mpk 2 resolved:starter package ships 4.0.0 / 2.2.1; install kept the starter's newer 5.1.0 / 2.4.0 (captures/16); no CE0463 +out app-setting Project security security level + user-role mapping 1 handoff source is Production with 8 user roles; the slice starter is Off with 3, and NativeNote/Synchronization module roles are mapped to no user role: the access-rule XPath through the stubs is never evaluated. Raise to Production and map Technician -> NativeNote.EditMy, Synchronization.CreateMy before any runtime test diff --git a/tests/wave2/fixtures/cut-ledger/cut-rules.tsv b/tests/wave2/fixtures/cut-ledger/cut-rules.tsv new file mode 100644 index 00000000..92316c83 --- /dev/null +++ b/tests/wave2/fixtures/cut-ledger/cut-rules.tsv @@ -0,0 +1,48 @@ +# cut-rules.tsv — decisions for the NativeNote slice (increment 3). directionpatternresolutionevidence +# pattern: fnmatch on the far-side doc, or 'SliceDoc -> FarDoc'. First match wins. Scripts: mdl/edges-*.mdl +# ---- OUT: the slice uses something that stays in the monolith +out NanoflowCommons.* resolved:marketplace NanoflowCommons 7.2.1 ships in the Blank Native Mobile App starter (captures/14); mx check 0 (captures/21) +out NativeMobileResources.* resolved:marketplace NativeMobileResources 12.2.2 ships in the starter; mx check 0 +out CommunityCommons.* resolved:marketplace marketplace install 170 -> 11.5.1 (captures/15) +out Utils.* resolved:mpk Utils moved by export-module-mpk.py + install --file (captures/16); cut-score says copy Utils to mobile; its own edges are --extra rows +out FSM_Specific.NativePhone_* resolved:copy-drift layout copied from describe; SafeAreaView wrapper dropped (mxcli refuses placeholders inside a pluggable-widget slot) — edges-B.mdl; iOS safe-area padding lost +out NativeMobile.StorageItem_* resolved:copy 2-activity nanoflow copied verbatim (deps NanoflowCommons, Utils present) — edges-B.mdl +out NativeMobile.ENUM_LoadingType resolved:copy enumeration copied verbatim — edges-B.mdl +out NativeMobile.NativeWorkorder_AppointmentDetailsHelper_AddNoteCount resolved:copy nanoflow copied verbatim; runs against the stub AppointmentDetailsHelper/NativeWorkOrder — edges-B.mdl +out NativeMobile.JS_GetOriginalValue stub JS action copied, generic type parameter narrowed to NativeNote.NativeNote (describe output Name(Entity: "Entity") does not parse; entity-type form fails CE0115) — same JS body, original .js file copied +out Note.JS_CloneFileDocument resolved:copy-retarget JS action copied into Synchronization (web module Note stays behind), the one call in OfflineImageAction_Create retargeted; original .js copied (describe drops the EXTRA CODE import of react-native Platform) +out NativeMobile.LoadHelper_* stub CUT STUB nanoflow, signature verbatim: StartLoading creates a LoadHelper, StopLoading returns — the monolith loading/all-done/error pages are NOT shown +out NativeMobile.NativeWorkOrder stub CUT STUB entity (SystemID only of 24 attributes); the slice has no work orders: synced to the device in the monolith (sync profile), here empty until a REST feed exists — HANDOFF to the landscape (NativeMobile co-locates in the Mobile app per cut-score) +out NativeMobile.NativeTicket stub CUT STUB entity (DisplayEnd only); *_DeleteOld XPath runs against an empty table +out NativeMobile.AppointmentDetailsHelper* stub CUT STUB non-persistent entity (NoteCount) + association; appointment details screen stays in the monolith +out NativeMobile.NativeWorkOrder_NativeTicket stub CUT STUB association (XPath in *_DeleteOld) +out NativeMobile._NativeWorkOrder_NativeTechnician_EditMy stub CUT STUB association used by NativeNote/NativeImage access-rule XPath; with no work orders, EditMy users see no notes — security HANDOFF with the data feed +out NativeTechnician.NativeTechnician stub CUT STUB entity (SystemID, TechnicianName); technician master data stays in the monolith — needs a REST/OData read or a synced copy +out Administration.Account_NativeTechnician stub CUT STUB association added to the starter's Administration 4.5.0 (FSM had modified its Administration module); a local edit to a marketplace module — re-apply after any Administration update +out Synchronization.Synchronize stub CUT STUB nanoflow (ShowMessageIfOffline): the monolith's app-wide sync is not carried; the slice's offline actions are created but never pushed — HANDOFF: a slice-local synchronize + server processing +# ---- CHECK-ONLY: names mx check reports that the catalog never recorded +check General.ACT_DoNothing_NF resolved:copy App events widget action (catalog blind spot) — copied, edges-B.mdl +check FSM_Specific.Material_Design_Icons resolved:copy page/layout icon images (catalog blind spot) — edges-A-icons.mdl +# ---- IN: the monolith uses something that moves (strangler work on the monolith side; nothing executed in this increment) +in Synchronization.OfflineAction* -> * resolved:copy generic offline-action base (entity, save, list commit) is used by all 16 offline action types: copied into the slice, the monolith keeps its own — no rewire +in Synchronization.UploadHelper* -> * resolved:copy generic upload bookkeeping, copied; the monolith keeps its own — no rewire +in Synchronization.* -> Synchronization.Offline*Action_Process* handoff server-side processing of uploaded note/image offline actions stays in the monolith (it writes web Note records): the slice must forward OfflineNoteAction/OfflineImageAction to it — REST push slice->monolith (the seam, device->web direction) +in Synchronization.* -> Synchronization.Offline*Action_Validate handoff server-side validation of uploaded note/image actions — goes with the processing seam above +in Synchronization.* -> Synchronization.Offline*Action_NewNote handoff creates the web Note from an offline note action — processing seam +in Synchronization.* -> Synchronization.Offline*Action_UpdateNote handoff updates the web Note from an offline note action — processing seam +in Synchronization.* -> NativeMobile.DS_GetObjectCounts resolved:copy device-side object counts over the offline-action types; the monolith keeps its copies of those entities (dead for notes once NativeNote retires there) +in Synchronization.* -> Navigation.NativePhone resolved:copy monolith profile syncs its own copies of NoteImage/Offline{Note,Image}Action; the slice profile syncs the slice copies (edges-C-app.mdl) +in Synchronization.* -> Synchronization.* resolved:copy monolith Synchronization docs referencing the generic offline-action types the slice copied; the monolith keeps its own copy — no rewire for this slice +in NativeNote.* -> Synchronization.Note_*NativeNote handoff Note->NativeNote server-side push (web note edited -> device copy updated) — becomes a REST/OData action into the slice (the intended seam) +in NativeNote.* -> Synchronization.NoteImage_*Native* handoff NoteImage->NativeImage server-side push — same seam as Note->NativeNote +in NativeNote.* -> Synchronization.Native*List_* handoff initial/periodic server-side fill of NativeNote/NativeImage from web notes — same seam (REST pull or push) +in NativeNote.* -> Synchronization.Rule_Note_HasChanged handoff change detection for the Note->NativeNote push — moves with the seam +in NativeNote.* -> Synchronization.* handoff monolith sync flow (work-order tree, other offline actions) naming NativeNote — rewire when NativeNote leaves the monolith (strangler step 5) +in NativeNote.* -> NativeModelCheck.* retire NativeModelCheck is test tooling (cut-score-findings: retire, 1 inbound edge) +in NativeNote.* -> MasterData.* handoff the one MasterData admin page (Note_NewEdit_MxAdmin + its 3 flows) stays in the monolith: needs an OData read/write of NativeNote from the slice +in NativeNote.* -> Navigation.NativePhone handoff monolith NativePhone profile syncs NativeNote/NativeImage: drop those lines when NativeNote retires there; the slice profile has them (edges-C-app.mdl) +in NativeNote.* -> NativeMobile.* handoff NativeMobile (work-order screens, counts, cleanup) opens/counts NativeNote: co-locate in the Mobile app (cut-score landscape) or deep-link into the slice +in NativeNote.* -> NativeMobileRelated.* handoff related-work tree reads NativeNote: same as NativeMobile +in NativeNote.* -> NativeTechnician.* handoff technician XPath to NativeNote: same as NativeMobile +in NativeNote.* -> Note.* handoff web Note delete cascades into NativeNote/NativeImage lists: becomes a REST delete into the slice (same seam) +in NativeNote.* -> General.* handoff General.TurnSoftDeleteToHard (scheduled cleanup) calls NativeNote cleanup: schedule it in the slice instead diff --git a/tests/wave2/fixtures/cut-ledger/expected-ledger.tsv b/tests/wave2/fixtures/cut-ledger/expected-ledger.tsv new file mode 100644 index 00000000..83b56fb8 --- /dev/null +++ b/tests/wave2/fixtures/cut-ledger/expected-ledger.tsv @@ -0,0 +1,58 @@ +edge direction kind source_doc target_doc refs resolution evidence +E001 out xpath NativeNote.NativeImage Administration.Account_NativeTechnician 1 stub CUT STUB association added to the starter's Administration 4.5.0 (FSM had modified its Administration module); a local edit to a marketplace module — re-apply after any Administration update +E006 out call NativeNote.NativeImage_DeleteOld CommunityCommons.EndTransaction 1 resolved:marketplace marketplace install 170 -> 11.5.1 (captures/15) +E008 out layout NativeNote.NativeImage_NewEdit FSM_Specific.NativePhone_FullPage_Back 1 resolved:copy-drift layout copied from describe; SafeAreaView wrapper dropped (mxcli refuses placeholders inside a pluggable-widget slot) — edges-B.mdl; iOS safe-area padding lost +E010 out call Synchronization.UploadHelper_Get NanoflowCommons.GetStorageItemObject 1 resolved:marketplace NanoflowCommons 7.2.1 ships in the Blank Native Mobile App starter (captures/14); mx check 0 (captures/21) +E016 out member NativeNote.NativeNote_ResetAppointmentDetailsHelper NativeMobile.AppointmentDetailsHelper 1 stub CUT STUB non-persistent entity (NoteCount) + association; appointment details screen stays in the monolith +E019 out value NativeNote.ACT_NativeNote_Delete NativeMobile.ENUM_LoadingType 1 resolved:copy enumeration copied verbatim — edges-B.mdl +E021 out call NativeNote.NativeNote_Get_OriginalValue NativeMobile.JS_GetOriginalValue 1 stub JS action copied, generic type parameter narrowed to NativeNote.NativeNote (describe output Name(Entity: "Entity") does not parse; entity-type form fails CE0115) — same JS body, original .js file copied +E022 out call NativeNote.ACT_NativeNote_Delete NativeMobile.LoadHelper_StartLoading 1 stub CUT STUB nanoflow, signature verbatim: StartLoading creates a LoadHelper, StopLoading returns — the monolith loading/all-done/error pages are NOT shown +E026 out xpath NativeNote.NativeImage_DeleteOld NativeMobile.NativeTicket 1 stub CUT STUB entity (DisplayEnd only); *_DeleteOld XPath runs against an empty table +E028 out return NativeNote.NativeNote_GetNativeWorkorder NativeMobile.NativeWorkOrder 1 stub CUT STUB entity (SystemID only of 24 attributes); the slice has no work orders: synced to the device in the monolith (sync profile), here empty until a REST feed exists — HANDOFF to the landscape (NativeMobile co-locates in the Mobile app per cut-score) +E030 out xpath NativeNote.NativeImage_DeleteOld NativeMobile.NativeWorkOrder_NativeTicket 1 stub CUT STUB association (XPath in *_DeleteOld) +E032 out call NativeNote.ACT_NativeNote_Delete NativeMobile.NativeWorkorder_AppointmentDetailsHelper_AddNoteCount 1 resolved:copy nanoflow copied verbatim; runs against the stub AppointmentDetailsHelper/NativeWorkOrder — edges-B.mdl +E033 out call Synchronization.UploadHelper_CheckAndGet NativeMobile.StorageItem_Check 1 resolved:copy 2-activity nanoflow copied verbatim (deps NanoflowCommons, Utils present) — edges-B.mdl +E035 out xpath NativeNote.NativeImage NativeMobile._NativeWorkOrder_NativeTechnician_EditMy 1 stub CUT STUB association used by NativeNote/NativeImage access-rule XPath; with no work orders, EditMy users see no notes — security HANDOFF with the data feed +E037 out value NativeNote.NativeNote_AddImage NativeMobileResources.PictureQuality 1 resolved:marketplace NativeMobileResources 12.2.2 ships in the starter; mx check 0 +E040 out associate NativeNote.NativeNote_NativeTechnician NativeTechnician.NativeTechnician 1 stub CUT STUB entity (SystemID, TechnicianName); technician master data stays in the monolith — needs a REST/OData read or a synced copy +E042 out call Synchronization.OfflineImageAction_Create Note.JS_CloneFileDocument 1 resolved:copy-retarget JS action copied into Synchronization (web module Note stays behind), the one call in OfflineImageAction_Create retargeted; original .js copied (describe drops the EXTRA CODE import of react-native Platform) +E043 out call NativeNote.ACT_NativeNote_Delete Synchronization.Synchronize 1 stub CUT STUB nanoflow (ShowMessageIfOffline): the monolith's app-wide sync is not carried; the slice's offline actions are created but never pushed — HANDOFF: a slice-local synchronize + server processing +E045 out member Synchronization.OfflineImageAction_Create System.FileDocument 1 platform System module exists in every Mendix app +E048 out call NativeNote.NativeNote_SetCreated Utils.Account_GetCurrent_NF 1 resolved:mpk Utils moved by export-module-mpk.py + install --file (captures/16); cut-score says copy Utils to mobile; its own edges are --extra rows +E056 in action NativeMobile.Snippet_NativeNoteList NativeNote.ACT_NativeNote_Create 1 handoff NativeMobile (work-order screens, counts, cleanup) opens/counts NativeNote: co-locate in the Mobile app (cut-score landscape) or deep-link into the slice +E057 in value Synchronization.NativeInstanceItem_SetupOfflinePictureActions NativeNote.ENUM_ImageAction 2 handoff monolith sync flow (work-order tree, other offline actions) naming NativeNote — rewire when NativeNote leaves the monolith (strangler step 5) +E064 in retrieve NativeMobileRelated.NativeNoteNP_GetTreeData NativeNote.NativeImage 1 handoff related-work tree reads NativeNote: same as NativeMobile +E066 in retrieve NativeModelCheck.DeleteNativeShadowModel NativeNote.NativeImage 1 retire NativeModelCheck is test tooling (cut-score-findings: retire, 1 inbound edge) +E071 in sync Navigation.NativePhone NativeNote.NativeImage 1 handoff monolith NativePhone profile syncs NativeNote/NativeImage: drop those lines when NativeNote retires there; the slice profile has them (edges-C-app.mdl) +E072 in parameter Synchronization.NativeImageList_AddAndSynchronize NativeNote.NativeImage 1 handoff initial/periodic server-side fill of NativeNote/NativeImage from web notes — same seam (REST pull or push) +E078 in change Synchronization.NoteImage_ProcessUpdatingNativeImage NativeNote.NativeImage 1 handoff NoteImage->NativeImage server-side push — same seam as Note->NativeNote +E084 in call Note.ImageList_Delete NativeNote.NativeImageList_Delete 1 handoff web Note delete cascades into NativeNote/NativeImage lists: becomes a REST delete into the slice (same seam) +E092 in call General.TurnSoftDeleteToHard NativeNote.NativeImage_TurnSoftDeleteToHard 1 handoff General.TurnSoftDeleteToHard (scheduled cleanup) calls NativeNote cleanup: schedule it in the slice instead +E093 in parameter MasterData.ACT_NativeNote_Save_MxAdmin NativeNote.NativeNote 1 handoff the one MasterData admin page (Note_NewEdit_MxAdmin + its 3 flows) stays in the monolith: needs an OData read/write of NativeNote from the slice +E111 in change Synchronization.Note_ProcessUpdatingNativeNote NativeNote.NativeNote 1 handoff Note->NativeNote server-side push (web note edited -> device copy updated) — becomes a REST/OData action into the slice (the intended seam) +E116 in parameter Synchronization.Rule_Note_HasChanged NativeNote.NativeNote 1 handoff change detection for the Note->NativeNote push — moves with the seam +E131 in xpath NativeTechnician.NativeTechnician NativeNote.NativeNote_NativeTechnician 1 handoff technician XPath to NativeNote: same as NativeMobile +E155 in retrieve NativeMobile.DS_GetObjectCounts Synchronization.NoteImage 1 resolved:copy device-side object counts over the offline-action types; the monolith keeps its copies of those entities (dead for notes once NativeNote retires there) +E156 in sync Navigation.NativePhone Synchronization.NoteImage 1 resolved:copy monolith profile syncs its own copies of NoteImage/Offline{Note,Image}Action; the slice profile syncs the slice copies (edges-C-app.mdl) +E157 in retrieve Synchronization.Private_SynchronizeOfflineActions Synchronization.NoteImage 1 resolved:copy monolith Synchronization docs referencing the generic offline-action types the slice copied; the monolith keeps its own copy — no rewire for this slice +E159 in parameter Form.InstanceForm_PrepareDelete_InstanceItems Synchronization.OfflineAction 1 resolved:copy generic offline-action base (entity, save, list commit) is used by all 16 offline action types: copied into the slice, the monolith keeps its own — no rewire +E273 in parameter Synchronization.OfflineImageAction_Process Synchronization.OfflineImageAction 1 handoff server-side processing of uploaded note/image offline actions stays in the monolith (it writes web Note records): the slice must forward OfflineNoteAction/OfflineImageAction to it — REST push slice->monolith (the seam, device->web direction) +E274 in parameter Synchronization.OfflineImageAction_Validate Synchronization.OfflineImageAction 1 handoff server-side validation of uploaded note/image actions — goes with the processing seam above +E282 in parameter Synchronization.OfflineNoteAction_NewNote Synchronization.OfflineNoteAction 1 handoff creates the web Note from an offline note action — processing seam +E284 in parameter Synchronization.OfflineNoteAction_UpdateNote Synchronization.OfflineNoteAction 1 handoff updates the web Note from an offline note action — processing seam +E287 in change Synchronization.UploadHelper_Set_LastUploadStartedDate Synchronization.UploadHelper 1 resolved:copy generic upload bookkeeping, copied; the monolith keeps its own — no rewire +E294 out check-only (mx check) FSM_Specific.Material_Design_Icons 5 resolved:copy page/layout icon images (catalog blind spot) — edges-A-icons.mdl +E295 out check-only (mx check) General.ACT_DoNothing_NF 5 resolved:copy App events widget action (catalog blind spot) — copied, edges-B.mdl +E296 out second-order Utils (mpk) ValidationFeedback.* 4 resolved:marketplace Utils' own edge; marketplace install 110282 -> 3.4.0 (captures/15) +E297 out second-order Utils (mpk) CommunityCommons.* 3 resolved:marketplace Utils' own edge; CommunityCommons 11.5.1 (captures/15) +E298 out second-order Utils (mpk) Administration.Account 3 resolved:marketplace Utils' own edge; starter's Administration 4.5.0 has Account +E299 out second-order Utils (mpk) NanoflowCommons.* 1 resolved:marketplace Utils' own edge; starter's NanoflowCommons 7.2.1 +E300 out second-order Utils (mpk) NativeMobileResources.* 2 resolved:marketplace Utils' own edge; starter's NativeMobileResources 12.2.2 +E301 out second-order Utils (mpk) AppSettings.Environment 1 stub CUT STUB constant (default 'Local', exposed to client) — edges-C-app.mdl; without it mx check CE0117 at Decision 'Production?' +E302 out java-handoff Utils (mpk) javasource/utils/actions/GetFileType.java 1 handoff:done this.context() is a Mendix 9 API -> getContext() (captures/23); mxbuild javac error before, BUILD SUCCEEDED after +E303 out java-handoff Utils (mpk) javasource/utils/actions/{JA_RoundMinutes,executeInBackgroundImmediately}.java 2 handoff:done orphan action files (no Java action unit in Utils) extending com.mendix.webui.CustomJavaAction — deleted (captures/23) +E304 out java-handoff Utils (mpk) userlib/tika-core-1.28.4.jar 1 handoff:done source RequiredLib marker names former module FileTypeChecker, so the exporter did not ship it; copied with a .Utils.RequiredLib marker (captures/23) +E305 out app-setting NativeNote pages Navigation.NativePhone (native profile) 1 resolved:starter mxcli cannot create a native profile (only the fixed web kinds) and without one every nanoflow show-page of a native page is CE6081: target is Blank Native Mobile App 11.13.0 (Marketplace 109511) via mx create-project, not mxcli new (captures/14) +E306 out app-setting Navigation.NativePhone sync lines for NativeNote, NativeImage, Offline{Note,Image}Action, NoteImage, NativeWorkOrder, NativeTicket, NativeTechnician, Account 9 resolved:mdl the package scrubs navigation; source lines replayed for the slice's entities — edges-C-app.mdl +E307 out widget NativeNote pages widgets/AppEvents.mpk, Repeater.mpk 2 resolved:starter package ships 4.0.0 / 2.2.1; install kept the starter's newer 5.1.0 / 2.4.0 (captures/16); no CE0463 +E308 out app-setting Project security security level + user-role mapping 1 handoff source is Production with 8 user roles; the slice starter is Off with 3, and NativeNote/Synchronization module roles are mapped to no user role: the access-rule XPath through the stubs is never evaluated. Raise to Production and map Technician -> NativeNote.EditMy, Synchronization.CreateMy before any runtime test diff --git a/tests/wave2/fixtures/cut-ledger/exporter-prefix.txt b/tests/wave2/fixtures/cut-ledger/exporter-prefix.txt new file mode 100644 index 00000000..37eadb07 --- /dev/null +++ b/tests/wave2/fixtures/cut-ledger/exporter-prefix.txt @@ -0,0 +1,56 @@ +$ python3 -I export-module-mpk.py mono/fsm.mpr NativeNote out924/NativeNote.mpk --json +{ + "module": "NativeNote", + "product_version": "9.24.2.6130", + "units_module_subtree": 73, + "units_shell": 6, + "project_docs_scrubbed": [ + "Settings$ProjectSettings", + "Navigation$NavigationDocument", + "Security$ProjectSecurity" + ], + "unit_types": { + "DomainModels$DomainModel": 1, + "Enumerations$Enumeration": 2, + "Forms$Page": 2, + "Microflows$Microflow": 16, + "Microflows$Nanoflow": 35, + "Projects$Folder": 14, + "Projects$ModuleImpl": 1, + "Projects$ModuleSettings": 1, + "Security$ModuleSecurity": 1 + }, + "from_app_store": null, + "files": 7, + "java_files": 0, + "js_files": 0, + "widgets_referenced": [ + "com.mendix.widget.native.appevents.AppEvents", + "com.mendix.widget.native.repeater.Repeater" + ], + "widgets_shipped": [ + "widgets/com.mendix.widget.native.AppEvents.mpk", + "widgets/com.mendix.widget.native.Repeater.mpk" + ], + "widgets_unresolved": [], + "crossing_refs_by_module": { + "Administration": 3, + "CommunityCommons": 2, + "FSM_Specific": 2, + "General": 1, + "NanoflowCommons": 4, + "NativeMobile": 8, + "NativeMobileResources": 1, + "NativeTechnician": 2, + "Note": 54, + "Synchronization": 5, + "Technician": 2, + "Utils": 7 + }, + "out": "out924/NativeNote.mpk" +} + +real 0m0.326s +user 0m0.222s +sys 0m0.076s +exit=0 diff --git a/tests/wave2/fixtures/cut-ledger/exporter.json b/tests/wave2/fixtures/cut-ledger/exporter.json new file mode 100644 index 00000000..a0c0e114 --- /dev/null +++ b/tests/wave2/fixtures/cut-ledger/exporter.json @@ -0,0 +1,48 @@ +{ + "module": "NativeNote", + "product_version": "9.24.2.6130", + "units_module_subtree": 73, + "units_shell": 6, + "project_docs_scrubbed": [ + "Settings$ProjectSettings", + "Navigation$NavigationDocument", + "Security$ProjectSecurity" + ], + "unit_types": { + "DomainModels$DomainModel": 1, + "Enumerations$Enumeration": 2, + "Forms$Page": 2, + "Microflows$Microflow": 16, + "Microflows$Nanoflow": 35, + "Projects$Folder": 14, + "Projects$ModuleImpl": 1, + "Projects$ModuleSettings": 1, + "Security$ModuleSecurity": 1 + }, + "from_app_store": null, + "files": 0, + "java_files": 0, + "js_files": 0, + "widgets_referenced": [ + "com.mendix.widget.native.appevents.AppEvents", + "com.mendix.widget.native.repeater.Repeater" + ], + "widgets_shipped": [], + "widgets_unresolved": [ + "com.mendix.widget.native.appevents.AppEvents", + "com.mendix.widget.native.repeater.Repeater" + ], + "crossing_refs_by_module": { + "Administration": 3, + "CommunityCommons": 2, + "FSM_Specific": 2, + "General": 1, + "NanoflowCommons": 4, + "NativeMobile": 8, + "NativeMobileResources": 1, + "NativeTechnician": 2, + "Synchronization": 5, + "Utils": 7 + }, + "out": "outfix/NativeNote-924.mpk" +} diff --git a/tests/wave2/fixtures/cut-ledger/mx-check.txt b/tests/wave2/fixtures/cut-ledger/mx-check.txt new file mode 100644 index 00000000..fbd2234a --- /dev/null +++ b/tests/wave2/fixtures/cut-ledger/mx-check.txt @@ -0,0 +1,10 @@ +[error] [CE1613] "The selected image 'FSM_Specific.Material_Design_Icons.close_FILL0_wght400_GRAD0_opsz24' no longer exists." at Action button 'actionButton8' +[error] [CE1613] "The selected image 'FSM_Specific.Material_Design_Icons.chevron_left_FILL0_wght400_GRAD0_opsz24_2' no longer exists." at Action button 'actionButton7' +[error] [CE1613] "The selected image 'FSM_Specific.Material_Design_Icons.delete_FILL0_wght400_GRAD0_opsz24' no longer exists." at Action button 'actionButton10' +[error] [CE1613] "The selected image 'FSM_Specific.Material_Design_Icons.edit_FILL0_wght400_GRAD0_opsz24' no longer exists." at Action button 'actionButton9' +[error] [CE1613] "The selected image 'FSM_Specific.Material_Design_Icons.delete_FILL0_wght400_GRAD0_opsz24' no longer exists." at Action button 'actionButton3' +[error] [CE1613] "The selected nanoflow 'General.ACT_DoNothing_NF' no longer exists." at App events 'appEvents1' +[error] [CE1613] "The selected nanoflow 'General.ACT_DoNothing_NF' no longer exists." at App events 'appEvents1' +[error] [CE1613] "The selected nanoflow 'General.ACT_DoNothing_NF' no longer exists." at App events 'appEvents1' +[error] [CE1613] "The selected nanoflow 'General.ACT_DoNothing_NF' no longer exists." at App events 'appEvents1' +[error] [CE1613] "The selected nanoflow 'General.ACT_DoNothing_NF' no longer exists." at App events 'appEvents1' diff --git a/tests/wave2/fixtures/cut-ledger/slice.txt b/tests/wave2/fixtures/cut-ledger/slice.txt new file mode 100644 index 00000000..3ba87359 --- /dev/null +++ b/tests/wave2/fixtures/cut-ledger/slice.txt @@ -0,0 +1,30 @@ +# increment 3 slice: what moves out of FSM into NoteSlice (Mendix 11.13.0) +NativeNote # whole module, by MPK (export-module-mpk.py + marketplace install --file) +# the Synchronization_Note flows: closure (inside Synchronization) of the 3 entry nanoflows NativeNote calls, +# moved by describe -> exec (MDL). Synchronization.Synchronize is NOT here: it is the app-wide sync, stubbed. +Synchronization.NativeNote_SetupOfflineNoteAction +Synchronization.NativeNote_SetupOfflineImageActions +Synchronization.NativeImage_SetupOfflineImageAction +Synchronization.OfflineNoteAction_Create +Synchronization.OfflineNoteAction_SetTechnicianSystemID +Synchronization.OfflineNoteAction_SetWorkOrderSystemID +Synchronization.OfflineImageAction_Create +Synchronization.NoteImage_Commit +Synchronization.OfflineAction_Save +Synchronization.OfflineActionList_Commit +Synchronization.OfflineActionList_Save_NF +Synchronization.ACO_NoteImage_SetHasBeenUploadedTrue +Synchronization.ACO_OfflineAction_SetHasBeenUploadedTrue +Synchronization.UploadHelper_CheckAndGet +Synchronization.UploadHelper_Create_StorageItemKey +Synchronization.UploadHelper_Get +Synchronization.UploadHelper_GetOrCreate +Synchronization.UploadHelper_Set_LastOfflineCreatedDate +Synchronization.UploadHelper_Store +Synchronization.UploadHelper_Update_LastOfflineCreatedDate +Synchronization.OfflineAction +Synchronization.OfflineNoteAction +Synchronization.OfflineImageAction +Synchronization.NoteImage +Synchronization.UploadHelper +Synchronization.OfflineImageAction_NoteImage diff --git a/tests/wave2/test-cut-ledger.sh b/tests/wave2/test-cut-ledger.sh new file mode 100644 index 00000000..7e4b1b4c --- /dev/null +++ b/tests/wave2/test-cut-ledger.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# test-cut-ledger.sh — pin bin/cut-ledger.sh against a CAPTURED slice (fixtures/cut-ledger/, see +# CAPTURE.txt there): a verbatim subset of #293's field run on a public field-service template app. +# No mxcli, no .mpr: the --refs form of the input, plus a catalog.db built from the same rows. +# +# 1. THE CAPTURED LEDGER IS REPRODUCED. 43 refs -> 57 rows whose columns 2-8 equal the field run's +# own rows, in the field run's order; E-numbers restart at E001. Counts on the summary line. +# 2. EVERY ROW HAS A DECISION. No UNRESOLVED, no unused rule; System is `platform` without a rule. +# 3. MX CHECK NAMES THE CATALOG NEVER SAW become check-only rows; names it did see do not. +# 4. THE EXPORTER IS RECONCILED. The fixed exporter JSON adds no note; the pre-fix transcript flags +# Technician exactly as the field run did. +# 5. NO DECISION, NO PASS. Drop the retire rule -> that row is UNRESOLVED and the run exits 1. +# A rule without evidence, an unknown resolution, a bad --extra row, an empty slice: exit 2. +# 6. --catalog READS THE SAME EDGES as --refs (single-tree and two-tree catalog paths); +# a database without a refs table is exit 2. --dump-refs writes the input back byte for byte. +# 7. DETERMINISTIC. +# +# Usage: bash test-cut-ledger.sh [path-to-cut-ledger.sh] + +set -u + +TOOLKIT="$(cd "$(dirname "$0")/../.." && pwd)" +CL="${1:-$TOOLKIT/bin/cut-ledger.sh}" +FIX="$TOOLKIT/tests/wave2/fixtures/cut-ledger" +# shellcheck disable=SC1091 +. "$TOOLKIT/bin/lib/portable.sh" +require_py + +PASS=0; FAIL=0 +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n %s\n' "$1" "${2:-}"; } +check(){ if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "expected '$3', got '$2'"; fi; } +has() { case "$2" in *"$3"*) ok "$1" ;; *) bad "$1" "no '$3' in: $2" ;; esac; } +hasnt(){ case "$2" in *"$3"*) bad "$1" "unexpected '$3' in: $2" ;; *) ok "$1" ;; esac; } + +BASE="--slice $FIX/slice.txt --check-log $FIX/mx-check.txt --extra $FIX/cut-extra.tsv" +# shellcheck disable=SC2086 +run() { bash "$CL" $BASE "$@"; } +col() { awk -F'\t' -v c="$2" 'NR>1{print $c}' "$1"; } + +echo "== 1. the captured ledger is reproduced ==" +run --refs "$FIX/crossing-refs.tsv" --rules "$FIX/cut-rules.tsv" --exporter-json "$FIX/exporter.json" \ + --dump-refs "$TMP/dump.tsv" -o "$TMP/l1.tsv" >"$TMP/o1" 2>&1; RC=$? +check "exits 0" "$RC" "0" +OUT="$(cat "$TMP/o1")" +has "summary counts" "$OUT" "43 crossing refs -> 57 ledger rows (20 out, 22 in from the catalog; 2 check-only; 13 extra)" +has "by-resolution counts" "$OUT" "by resolution: handoff 20, platform 1, resolved 24, retire 1, stub 11" +check "header" "$(head -1 "$TMP/l1.tsv")" "$(printf 'edge\tdirection\tkind\tsource_doc\ttarget_doc\trefs\tresolution\tevidence')" +check "57 rows" "$(($(wc -l <"$TMP/l1.tsv") - 1))" "57" +cut -f2- "$TMP/l1.tsv" >"$TMP/got"; cut -f2- "$FIX/expected-ledger.tsv" >"$TMP/want" +if cmp -s "$TMP/got" "$TMP/want"; then ok "columns 2-8 equal the field run's rows, same order" +else bad "columns 2-8 equal the field run's rows" "$(diff "$TMP/want" "$TMP/got" | head -4)"; fi +check "E-numbers run E001..E057" "$(col "$TMP/l1.tsv" 1 | head -1)..$(col "$TMP/l1.tsv" 1 | tail -1)" "E001..E057" +check "the 42 catalog rows carry all 43 crossing refs (the denominator)" \ + "$(awk -F'\t' 'NR>1 && NR<=43 {s+=$6} END{print s+0}' "$TMP/l1.tsv")" "43" + +echo "== 2. every row has a decision ==" +check "no UNRESOLVED rows" "$(col "$TMP/l1.tsv" 7 | grep -c '^UNRESOLVED')" "0" +hasnt "no rule left unused" "$OUT" "rules never used" +check "System rows are platform" "$(awk -F'\t' 'NR>1 && $5 ~ /^System\./ {print $7}' "$TMP/l1.tsv" | sort -u)" "platform" +check "out rows 20+2+13, in rows 22" "$(col "$TMP/l1.tsv" 2 | sort | uniq -c | awk '{printf "%s=%s ", $2, $1}')" "in=22 out=35 " + +echo "== 3. mx check names the catalog never saw ==" +check "2 check-only rows" "$(awk -F'\t' '$3=="check-only"' "$TMP/l1.tsv" | wc -l | tr -d ' ')" "2" +check "they name the two catalog-blind docs" \ + "$(awk -F'\t' '$3=="check-only"{print $5}' "$TMP/l1.tsv" | tr '\n' ' ')" "FSM_Specific.Material_Design_Icons General.ACT_DoNothing_NF " +check "names the catalog already has are not repeated" \ + "$(awk -F'\t' '$3=="check-only" && $5 ~ /^(NativeTechnician|NativeMobile|Administration)\./' "$TMP/l1.tsv" | wc -l | tr -d ' ')" "0" + +echo "== 4. the exporter is reconciled ==" +hasnt "fixed exporter JSON: no exporter-only note" "$OUT" "exporter-only" +run --refs "$FIX/crossing-refs.tsv" --rules "$FIX/cut-rules.tsv" --exporter-json "$FIX/exporter-prefix.txt" \ + -o "$TMP/l4.tsv" >"$TMP/o4" 2>&1 +check "pre-fix exporter: exactly one note" "$(grep -c 'exporter-only' "$TMP/o4")" "1" +has "it is the field run's Technician note" "$(cat "$TMP/o4")" \ + "exporter-only: Technician: exporter counts 2 unit(s) naming it, ledger has 0 out-edges to it" + +echo "== 5. no decision, no pass ==" +grep -v 'NativeModelCheck' "$FIX/cut-rules.tsv" >"$TMP/rules-noretire.tsv" +run --refs "$FIX/crossing-refs.tsv" --rules "$TMP/rules-noretire.tsv" -o "$TMP/l5.tsv" >"$TMP/o5" 2>&1; RC=$? +check "a crossing edge without a rule: exit 1" "$RC" "1" +check "that one row reads UNRESOLVED" "$(col "$TMP/l5.tsv" 7 | grep -c '^UNRESOLVED')" "1" +has "the run names the count" "$(cat "$TMP/o5")" "UNRESOLVED rows: 1" +has "the row says no rule matched" "$(awk -F'\t' '$7=="UNRESOLVED"{print $8}' "$TMP/l5.tsv")" "no rule in rules-noretire.tsv matches NativeModelCheck." +# a captured rule with its evidence blanked / its resolution replaced +awk -F'\t' 'BEGIN{OFS="\t"} !/^#/ && !d {$4=""; d=1} {print}' "$FIX/cut-rules.tsv" >"$TMP/rules-noev.tsv" +run --refs "$FIX/crossing-refs.tsv" --rules "$TMP/rules-noev.tsv" -o "$TMP/x.tsv" >"$TMP/o5b" 2>&1; RC=$? +check "a rule without evidence: exit 2" "$RC" "2" +has "refusal says why" "$(cat "$TMP/o5b")" "a resolution without evidence is a claim" +awk -F'\t' 'BEGIN{OFS="\t"} !/^#/ && !d {$3="maybe"; d=1} {print}' "$FIX/cut-rules.tsv" >"$TMP/rules-badres.tsv" +run --refs "$FIX/crossing-refs.tsv" --rules "$TMP/rules-badres.tsv" -o "$TMP/x.tsv" >/dev/null 2>&1; RC=$? +check "an unknown resolution: exit 2" "$RC" "2" +awk -F'\t' 'BEGIN{OFS="\t"} !/^#/ && !d {$7=""; d=1} {print}' "$FIX/cut-extra.tsv" >"$TMP/extra-noev.tsv" +bash "$CL" --refs "$FIX/crossing-refs.tsv" --slice "$FIX/slice.txt" --rules "$FIX/cut-rules.tsv" \ + --extra "$TMP/extra-noev.tsv" -o "$TMP/x.tsv" >/dev/null 2>&1; RC=$? +check "an --extra row without evidence: exit 2" "$RC" "2" +grep '^#' "$FIX/slice.txt" >"$TMP/slice-empty.txt" +bash "$CL" --refs "$FIX/crossing-refs.tsv" --slice "$TMP/slice-empty.txt" --rules "$FIX/cut-rules.tsv" -o "$TMP/x.tsv" >/dev/null 2>&1; RC=$? +check "a slice that names nothing: exit 2" "$RC" "2" +bash "$CL" >/dev/null 2>&1; RC=$? +check "no arguments: exit 2" "$RC" "2" +bash "$CL" --refs "$FIX/crossing-refs.tsv" --slice "$FIX/slice.txt" -o "$TMP/x.tsv" >/dev/null 2>&1; RC=$? +check "missing --rules: exit 2" "$RC" "2" + +echo "== 6. --catalog reads the same edges; --dump-refs round-trips ==" +if cmp -s "$TMP/dump.tsv" "$FIX/crossing-refs.tsv"; then ok "--dump-refs writes the input back byte for byte" +else bad "--dump-refs round-trip" "$(diff "$FIX/crossing-refs.tsv" "$TMP/dump.tsv" | head -4)"; fi +for layout in single two; do + if [ "$layout" = single ]; then DB="$TMP/p-$layout/.mxcli/catalog.db"; else DB="$TMP/p-$layout/app/.mxcli/catalog.db"; fi + mkdir -p "$(dirname "$DB")" + "$PY" -c 'import sqlite3, sys +db = sqlite3.connect(sys.argv[2]) +db.execute("create table refs (SourceType text, SourceName text, TargetType text, TargetName text, RefKind text, ModuleName text)") +rows = [l.rstrip("\n").split("\t") for l in open(sys.argv[1], encoding="utf-8")][1:] +db.executemany("insert into refs values (?,?,?,?,?,?)", [r + [r[1].split(".")[0]] for r in rows]) +db.commit()' "$FIX/crossing-refs.tsv" "$DB" + run --catalog "$DB" --rules "$FIX/cut-rules.tsv" --exporter-json "$FIX/exporter.json" -o "$TMP/l6-$layout.tsv" >/dev/null 2>&1; RC=$? + check "$layout-tree catalog path: exit 0" "$RC" "0" + if cmp -s "$TMP/l6-$layout.tsv" "$TMP/l1.tsv"; then ok "$layout-tree: --catalog ledger equals the --refs ledger" + else bad "$layout-tree: --catalog ledger equals the --refs ledger" "$(diff "$TMP/l1.tsv" "$TMP/l6-$layout.tsv" | head -3)"; fi +done +"$PY" -c 'import sqlite3, sys; sqlite3.connect(sys.argv[1]).execute("create table other (x)")' "$TMP/norefs.db" +run --catalog "$TMP/norefs.db" --rules "$FIX/cut-rules.tsv" -o "$TMP/x.tsv" >"$TMP/o6" 2>&1; RC=$? +check "a database without a refs table: exit 2" "$RC" "2" +has "refusal names the missing table" "$(cat "$TMP/o6")" "has no refs table" +printf 'Source\tTarget\n' >"$TMP/badhead.tsv" +run --refs "$TMP/badhead.tsv" --rules "$FIX/cut-rules.tsv" -o "$TMP/x.tsv" >/dev/null 2>&1; RC=$? +check "a refs TSV with the wrong header: exit 2" "$RC" "2" + +echo "== 7. deterministic ==" +run --refs "$FIX/crossing-refs.tsv" --rules "$FIX/cut-rules.tsv" --exporter-json "$FIX/exporter.json" -o "$TMP/l7.tsv" >/dev/null 2>&1 +if cmp -s "$TMP/l7.tsv" "$TMP/l1.tsv"; then ok "two runs are byte-identical"; else bad "two runs are byte-identical"; fi + +echo +echo "$PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] From 401655361302559fa59c76b2342451889659ae8d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 08:12:25 +0000 Subject: [PATCH 2/2] decompose: cut-ledger review fixes (#345) - An unreadable input (missing --rules, nonexistent --catalog, malformed --exporter-json) exits 2, not the UNRESOLVED code 1. - A --refs line with the wrong field count is refused (exit 2, counted, first line named) instead of silently dropped. - test-cut-ledger 38 -> 44. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 2 +- bin/cut-ledger.sh | 3 ++- bin/lib/cut-ledger.py | 19 +++++++++++++++++-- tests/wave2/test-cut-ledger.sh | 15 +++++++++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb271fb4..adb3839e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased -- new(cut-ledger): **`bin/cut-ledger.sh` writes the cut ledger of one decompose slice — one row per crossing edge with its resolution (resolved / stub / handoff / retire / platform) and evidence — and exits 1 while any edge has none** (split from [#293](https://github.com/MendixMau/mxcli-project-toolkit/pull/293), 2/6). Reads the source app's catalog `refs` table (or a `--refs` TSV), a slice file (modules and single documents), first-match rules with mandatory evidence, `mx check` CE1613 names the catalog never recorded (check-only rows), the module exporter's JSON (modules it counts with no ledger row are printed), and verbatim `--extra` rows. #293 shipped it with no fixture; new `tests/wave2/test-cut-ledger.sh` (38 checks) runs on a captured line subset of #293's field run (43 of 346 crossing refs, one key per rule) and reproduces that run's own 57 ledger rows column for column, the pre-fix exporter note, UNRESOLVED → exit 1, refusals → exit 2, and `--catalog` at single-tree and `app/` paths. Header corrected to the run's real counts (346 refs → 308 rows; it said 199). — MendixMau (#293 field run on a public template app) +- new(cut-ledger): **`bin/cut-ledger.sh` writes the cut ledger of one decompose slice — one row per crossing edge with its resolution (resolved / stub / handoff / retire / platform) and evidence — and exits 1 while any edge has none** (split from [#293](https://github.com/MendixMau/mxcli-project-toolkit/pull/293), 2/6). Reads the source app's catalog `refs` table (or a `--refs` TSV), a slice file (modules and single documents), first-match rules with mandatory evidence, `mx check` CE1613 names the catalog never recorded (check-only rows), the module exporter's JSON (modules it counts with no ledger row are printed), and verbatim `--extra` rows. #293 shipped it with no fixture; new `tests/wave2/test-cut-ledger.sh` (44 checks) runs on a captured line subset of #293's field run (43 of 346 crossing refs, one key per rule) and reproduces that run's own 57 ledger rows column for column, the pre-fix exporter note, UNRESOLVED → exit 1, refusals → exit 2 (now also an unreadable `--rules`/`--catalog`/`--exporter-json`, which escaped as exceptions with the UNRESOLVED code 1, and a `--refs` line with the wrong field count, which was silently dropped), and `--catalog` at single-tree and `app/` paths. Header corrected to the run's real counts (346 refs → 308 rows; it said 199). — MendixMau (#293 field run on a public template app) - process(docs): **1.0 release notes, a versioning rule, and a README for adopters.** New `docs/releases/v1.0.md`: the first named milestone, covering 2026-09-30 to 2026-10-14 by theme (independent review before merge, baseline words 79,997 → 76,259, exec.sh lock and restart verdicts, lint batches 1–4 with SEC014 and the stock-revert guard, journey/e2e fixes, the Stage 3 interaction model, delivery rounds), with upgrade steps, known gaps and credits. `docs/RELEASE-PROCESS.md` §8 is the single source for versioning: a milestone (1.0, 1.1, 2.0) is a train release the maintainer also names, with curated notes, a GitHub Release and a CHANGELOG pointer; major = a consuming project breaks without action (needs a migration section and a sync path), minor = no action needed; deprecations are announced in one milestone and removed no earlier than the next major; the `vYYYY.MM.DD` train and `bin/cut-release.sh` are unchanged; plus how a consumer checks, upgrades and pins a version. The header above now defers to it. README rewritten for newcomers: what it is, how it works, ways to use it, quick start, Make it yours (company brain, CLAUDE.local.md, lint opt-outs, waivers and --adopt, interview mode, exec approval and permission profiles, agent stubs, other harnesses), testing, a call for input, releases. The earlier material sits under Reference with the generated routing blocks untouched. Corrected: the Windows `pgrep` hazard (replaced by the per-project exec lock in #304), the hand-edit steps in "How to add a new skill" (tables are generated), and the stale Stage 6 and `examples/` notes. — MendixMau - fix(interview-mode): **`attended` and `unattended` are now read as aliases for `steering` and `auto` — a project that opted into unattended as documented was silently run as steering.** CLAUDE.md, the runbook and intake Q9 taught `Interview mode: unattended`, but `bin/interview-mode.sh` knew only `steering | assist | auto` and fell back to steering. The aliases are accepted in every source (env, `.interview-mode`, `PROJECT.md`, `--set`) under the same parse rules, with one stderr note. The `PROJECT.md` value is read after stripping bold/backticks: a bare mode or alias (`**auto**`, `` `auto` ``, `auto.`) resolves, a note may follow `steering`, but `assist`/`auto` with any trailing text falls back to steering with a "must stand alone on the line" message — so an annotated or hedged line never resolves quieter than before (two hedge word lists were tried in review and both leaked); env, session file and `--set` stay exact. Output stays canonical so `interview-gate.sh` is unchanged, and any other value still falls back to steering. Docs now name the canonical values (vocabulary in `interview-protocol.md`); intake Q9's body offers the three modes with steering the default (heading kept: it is sync's locator), and `sync-project.sh` reads an earlier unanswered Q9 body as unanswered rather than "edited". `test-interview-mode.sh` +57 checks (19 → 76) — MendixMau (found in the 1.0 README review) - fix(dispatching, sync-project, obligation-check): **review follow-ups for the 2026-10-14 integration batch.** `dispatching.md` names app-restart exits 4 and 5 beside 3 and 1. `test-test-scratch.sh` drops `ls | grep -q` under pipefail for here-strings; sync step 4d no longer aborts on empty doctor output; `_ob_has_denominator` strips NUL bytes so bash stops warning. — integration 2026-10-14 reviews diff --git a/bin/cut-ledger.sh b/bin/cut-ledger.sh index 580c4a95..28db54d9 100755 --- a/bin/cut-ledger.sh +++ b/bin/cut-ledger.sh @@ -47,7 +47,8 @@ # for a stub it never is — the evidence column says what the stub does not carry. Java and # JavaScript bodies naming another module, and references by string (deep links, REST paths). # -# Exit: 0 every row has a resolution · 1 at least one UNRESOLVED row · 2 bad arguments +# Exit: 0 every row has a resolution · 1 at least one UNRESOLVED row (and only that) · 2 bad arguments, +# an unreadable input (missing file, not a catalog, malformed JSON) or a malformed --refs line # # Field run: public field-service template app 9.24.2 -> its NativeNote slice into Mendix 11.13.0 # (2026-10-12): 346 crossing refs -> 308 ledger rows, 0 UNRESOLVED, 0.15 s. A captured subset of that diff --git a/bin/lib/cut-ledger.py b/bin/lib/cut-ledger.py index 9155dd98..f89d3ff4 100644 --- a/bin/lib/cut-ledger.py +++ b/bin/lib/cut-ledger.py @@ -54,10 +54,19 @@ def read_refs(args): head = f.readline().rstrip("\n").split("\t") if head != REFS_COLS: die("cut-ledger: %s header must be %s" % (args.refs, "\t".join(REFS_COLS))) - for line in f: + bad = [] + for n, line in enumerate(f, 2): + if not line.strip(): + continue v = line.rstrip("\n").split("\t") if len(v) == len(REFS_COLS): rows.append(dict(zip(REFS_COLS, v))) + else: + bad.append(n) + if bad: + # A dropped line is a crossing edge nobody decided on; refusing beats a short ledger. + die("cut-ledger: %s has %d line(s) without %d tab-separated fields (first: line %d)" + % (args.refs, len(bad), len(REFS_COLS), bad[0])) return rows @@ -215,4 +224,10 @@ def main(): if __name__ == "__main__": - sys.exit(main()) + # Exit 1 means "an edge is UNRESOLVED" and nothing else: an unreadable input (missing file, a + # path that is not a catalog, malformed exporter JSON) is a bad argument, exit 2, never 1. + try: + rc = main() + except (OSError, ValueError, sqlite3.Error) as e: + die("cut-ledger: cannot read an input: %s: %s" % (type(e).__name__, e)) + sys.exit(rc) diff --git a/tests/wave2/test-cut-ledger.sh b/tests/wave2/test-cut-ledger.sh index 7e4b1b4c..cd3b2541 100644 --- a/tests/wave2/test-cut-ledger.sh +++ b/tests/wave2/test-cut-ledger.sh @@ -104,6 +104,21 @@ check "no arguments: exit 2" "$RC" "2" bash "$CL" --refs "$FIX/crossing-refs.tsv" --slice "$FIX/slice.txt" -o "$TMP/x.tsv" >/dev/null 2>&1; RC=$? check "missing --rules: exit 2" "$RC" "2" +echo "== 5b. an unreadable input is exit 2, never the UNRESOLVED exit 1 (review of #345) ==" +run --refs "$FIX/crossing-refs.tsv" --rules "$TMP/no-such-rules.tsv" -o "$TMP/x.tsv" >"$TMP/o5c" 2>&1; RC=$? +check "a missing --rules file: exit 2" "$RC" "2" +has "it names the unreadable input" "$(cat "$TMP/o5c")" "cannot read an input" +run --catalog "$TMP/no-such-catalog.db" --rules "$FIX/cut-rules.tsv" -o "$TMP/x.tsv" >/dev/null 2>&1; RC=$? +check "a nonexistent --catalog: exit 2" "$RC" "2" +printf 'not json at all\n' >"$TMP/bad-exporter.json" +run --refs "$FIX/crossing-refs.tsv" --rules "$FIX/cut-rules.tsv" --exporter-json "$TMP/bad-exporter.json" -o "$TMP/x.tsv" >/dev/null 2>&1; RC=$? +check "an invalid --exporter-json: exit 2" "$RC" "2" +# a captured refs file with one line cut short (its last field dropped) +awk -F'\t' 'BEGIN{OFS="\t"} NR==5 {NF=4} {print}' "$FIX/crossing-refs.tsv" >"$TMP/short-line.tsv" +run --refs "$TMP/short-line.tsv" --rules "$FIX/cut-rules.tsv" -o "$TMP/x.tsv" >"$TMP/o5d" 2>&1; RC=$? +check "a --refs line with the wrong field count: exit 2" "$RC" "2" +has "the refusal counts it and names the line" "$(cat "$TMP/o5d")" "1 line(s) without 5 tab-separated fields (first: line 5)" + echo "== 6. --catalog reads the same edges; --dump-refs round-trips ==" if cmp -s "$TMP/dump.tsv" "$FIX/crossing-refs.tsv"; then ok "--dump-refs writes the input back byte for byte" else bad "--dump-refs round-trip" "$(diff "$FIX/crossing-refs.tsv" "$TMP/dump.tsv" | head -4)"; fi