From 0666692198bb8799b8323ce208e5f3eea9c0ce97 Mon Sep 17 00:00:00 2001 From: Patryk Gensch <43010113+patryk025@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:15:04 +0200 Subject: [PATCH] feat: add detailed method changes in compatibility analysis and update tests --- README.md | 7 ++-- ams/compat.py | 89 ++++++++++++++++++++++++++++++++++++++++++-- tests/test_compat.py | 5 +++ 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 245eed2..2a9e8fb 100644 --- a/README.md +++ b/README.md @@ -239,9 +239,10 @@ curl -OJ "http://127.0.0.1:8000/compat?format=json" # → compat.json (grupy ``` W UI: przyciski **⬇ md** / **⬇ json** w panelu *Gry / wersje*. Markdown to macierz `biblioteka × gra` -(`✓` = obecna; `A`/`B*`/… gdy nazwa ma kilka wariantów powierzchni) plus sekcja *Warianty* z -przypisem: dla każdego rozbieżnego wariantu overlap % i `types/methods/events/fields +N/-N/~N` -względem referencyjnego. +(`✓` = obecna; `A`/`B*`/… gdy nazwa ma kilka wariantów powierzchni) plus sekcja *Warianty*: dla +każdego rozbieżnego wariantu overlap %, zbiorcze `types/methods/events/fields +N/-N/~N`, a pod tym +**nazwany rozkład per klasa** — które metody/eventy/pola doszły (`+`), zniknęły (`−`) lub zmieniły +się (`~`), np. `CMC_Animo: −PAUSE ~FPS (type int→float)`. Pełne listy (bez cap-a) są w `vs_reference.detail` JSON-a. ## Front — Command Center diff --git a/ams/compat.py b/ams/compat.py index 3f43de9..f927a56 100644 --- a/ams/compat.py +++ b/ams/compat.py @@ -66,14 +66,55 @@ def _axis_counts(block: dict[str, Any]) -> dict[str, int]: } +def _changes_str(changes: dict[str, list]) -> str: + return ", ".join("{0} {1}→{2}".format(f, v[0], v[1]) for f, v in sorted(changes.items())) + + +def _types_named(block: dict[str, Any]) -> dict[str, list[str]]: + """Type-axis changes as display strings (types are global, not owner-scoped).""" + return { + "added": sorted("{0} → {1} (size {2})".format( + t["script_name"], t.get("cpp_class") or "?", t.get("object_size")) for t in block["added"]), + "removed": sorted(t["script_name"] for t in block["removed"]), + "changed": sorted("{0} ({1})".format(c["item"]["script_name"], _changes_str(c["changes"])) + for c in block["changed"]), + } + + +def _owner_named(block: dict[str, Any]) -> dict[str, Any]: + """Owner-scoped axis (methods/events/fields) grouped by class, so the diff reads as + 'which class gained/lost/changed which member' — the bit that was missing from the counts.""" + by: dict[str, dict[str, list[str]]] = {} + + def slot(owner: str) -> dict[str, list[str]]: + return by.setdefault(owner or "?", {"added": [], "removed": [], "changed": []}) + + for it in block["added"]: + slot(it["owner"])["added"].append(it["name"]) + for it in block["removed"]: + slot(it["owner"])["removed"].append(it["name"]) + for c in block["changed"]: + it = c["item"] + slot(it["owner"])["changed"].append("{0} ({1})".format(it["name"], _changes_str(c["changes"]))) + + return {"by_owner": {o: {k: sorted(v) for k, v in d.items()} for o, d in sorted(by.items())}} + + def _vs_reference(reference: Snapshot, variant: Snapshot) -> dict[str, Any]: - """How a divergent variant differs from its library's reference variant: per-axis +/-/~ counts - plus the pooled surface-overlap score, reusing the existing diff/similarity engines.""" + """How a divergent variant differs from its library's reference variant: per-axis +/-/~ counts, + the pooled surface-overlap score, and a `detail` block naming *which* classes/methods/events/ + fields changed (so the table can say 'CMC_Animo lost SHOW', not just 'methods −1').""" diff = compute_diff(reference, variant) return { "overall": surface_similarity(reference, variant)["overall"], "axes": {axis: _axis_counts(diff[axis]) for axis in _AXES}, "moved_methods": len(diff["moved_methods"]), + "detail": { + "types": _types_named(diff["types"]), + "methods": _owner_named(diff["methods"]), + "events": _owner_named(diff["events"]), + "fields": _owner_named(diff["fields"]), + }, } @@ -216,6 +257,46 @@ def _short(sha: str | None) -> str: return (sha or "?")[:8] +def _signed(sign: str, items: list[str], cap: int = 15) -> str: + head = [sign + it for it in items[:cap]] + if len(items) > cap: + head.append("…+{0}".format(len(items) - cap)) + return ", ".join(head) + + +def _emit_detail(out: list[str], vs: dict[str, Any]) -> None: + """Nested, named breakdown of a divergent variant: which types changed, and per class which + methods/events/fields were added (`+`), removed (`−`) or changed (`~`).""" + det = vs.get("detail") or {} + + t = det.get("types") or {} + toks = [] + if t.get("added"): + toks.append(_signed("+", t["added"])) + if t.get("removed"): + toks.append(_signed("−", t["removed"])) + if t.get("changed"): + toks.append(_signed("~", t["changed"])) + if toks: + out.append(" - typy: {0}".format(" ".join(toks))) + + for axis, label in (("methods", "metody"), ("events", "eventy"), ("fields", "pola")): + by = (det.get(axis) or {}).get("by_owner") or {} + if not by: + continue + out.append(" - {0} (klasy ze zmianami):".format(label)) + for owner in by: # already sorted at build time + d = by[owner] + parts = [] + if d.get("added"): + parts.append(_signed("+", d["added"])) + if d.get("removed"): + parts.append(_signed("−", d["removed"])) + if d.get("changed"): + parts.append(_signed("~", d["changed"])) + out.append(" - **{0}**: {1}".format(owner, " ".join(parts))) + + def render_markdown(table: dict[str, Any]) -> str: games = table["games"] libs = table["libraries"] @@ -224,7 +305,8 @@ def render_markdown(table: dict[str, Any]) -> str: "Grupowane po nazwie biblioteki. Komórka: pusta = brak; `✓` = obecna (jedyny wariant); " "litera wariantu (`A`, `B\\*`, …) gdy ta sama nazwa biblioteki ma **różne powierzchnie " "silnika** — `\\*` oznacza rozbieżność względem wariantu referencyjnego (`A`). " - "Szczegóły rozbieżności w sekcji *Warianty* niżej." + "Sekcja *Warianty* niżej wymienia per klasa **które** metody/eventy/pola doszły (`+`), " + "zniknęły (`−`) lub się zmieniły (`~`)." ) out.append("") out.append("_Wygenerowano: {0} · gier: {1} · bibliotek: {2}_".format( @@ -276,6 +358,7 @@ def render_markdown(table: dict[str, Any]) -> str: moved = " · przeniesionych metod {0}".format(vs["moved_methods"]) if vs["moved_methods"] else "" out.append( " - vs **A**: overlap {0}% · {1}{2}".format(vs["overall"], deltas, moved)) + _emit_detail(out, vs) out.append("") return "\n".join(out).rstrip() + "\n" diff --git a/tests/test_compat.py b/tests/test_compat.py index 9c0ea0e..7e115de 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -72,6 +72,9 @@ def test_divergent_surface_raises_asterisk(): assert [m["game"] for m in div["members"]] == ["Game C"] assert div["vs_reference"]["axes"]["methods"] == {"added": 1, "removed": 0, "changed": 0} assert 0 < div["vs_reference"]["overall"] < 100 + # named detail: *which* class gained *which* method + by_owner = div["vs_reference"]["detail"]["methods"]["by_owner"] + assert by_owner["CMC_Animo"]["added"] == ["PAUSE"] assert lib["cells"]["Game A"]["diverges"] is False assert lib["cells"]["Game C"]["diverges"] is True @@ -116,6 +119,8 @@ def test_markdown_has_matrix_and_asterisk(): assert "PIKLIB8.dll" in md assert "\\*" in md # the divergence marker survives to the table assert "vs **A**" in md # the footnote carries the delta + assert "**CMC_Animo**" in md # named per-class breakdown + assert "+PAUSE" in md # the concrete added method def test_json_round_trips():