feat: add detailed method changes in compatibility analysis and update tests

This commit is contained in:
Patryk Gensch
2026-06-19 00:15:04 +02:00
parent 429aafb6ce
commit 0666692198
3 changed files with 95 additions and 6 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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():