feat: add detailed method changes in compatibility analysis and update tests
This commit is contained in:
@@ -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`
|
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
|
(`✓` = obecna; `A`/`B*`/… gdy nazwa ma kilka wariantów powierzchni) plus sekcja *Warianty*: dla
|
||||||
przypisem: dla każdego rozbieżnego wariantu overlap % i `types/methods/events/fields +N/-N/~N`
|
każdego rozbieżnego wariantu overlap %, zbiorcze `types/methods/events/fields +N/-N/~N`, a pod tym
|
||||||
względem referencyjnego.
|
**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
|
## Front — Command Center
|
||||||
|
|
||||||
|
|||||||
@@ -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]:
|
def _vs_reference(reference: Snapshot, variant: Snapshot) -> dict[str, Any]:
|
||||||
"""How a divergent variant differs from its library's reference variant: per-axis +/-/~ counts
|
"""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."""
|
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)
|
diff = compute_diff(reference, variant)
|
||||||
return {
|
return {
|
||||||
"overall": surface_similarity(reference, variant)["overall"],
|
"overall": surface_similarity(reference, variant)["overall"],
|
||||||
"axes": {axis: _axis_counts(diff[axis]) for axis in _AXES},
|
"axes": {axis: _axis_counts(diff[axis]) for axis in _AXES},
|
||||||
"moved_methods": len(diff["moved_methods"]),
|
"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]
|
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:
|
def render_markdown(table: dict[str, Any]) -> str:
|
||||||
games = table["games"]
|
games = table["games"]
|
||||||
libs = table["libraries"]
|
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); "
|
"Grupowane po nazwie biblioteki. Komórka: pusta = brak; `✓` = obecna (jedyny wariant); "
|
||||||
"litera wariantu (`A`, `B\\*`, …) gdy ta sama nazwa biblioteki ma **różne powierzchnie "
|
"litera wariantu (`A`, `B\\*`, …) gdy ta sama nazwa biblioteki ma **różne powierzchnie "
|
||||||
"silnika** — `\\*` oznacza rozbieżność względem wariantu referencyjnego (`A`). "
|
"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("")
|
||||||
out.append("_Wygenerowano: {0} · gier: {1} · bibliotek: {2}_".format(
|
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 ""
|
moved = " · przeniesionych metod {0}".format(vs["moved_methods"]) if vs["moved_methods"] else ""
|
||||||
out.append(
|
out.append(
|
||||||
" - vs **A**: overlap {0}% · {1}{2}".format(vs["overall"], deltas, moved))
|
" - vs **A**: overlap {0}% · {1}{2}".format(vs["overall"], deltas, moved))
|
||||||
|
_emit_detail(out, vs)
|
||||||
out.append("")
|
out.append("")
|
||||||
|
|
||||||
return "\n".join(out).rstrip() + "\n"
|
return "\n".join(out).rstrip() + "\n"
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ def test_divergent_surface_raises_asterisk():
|
|||||||
assert [m["game"] for m in div["members"]] == ["Game C"]
|
assert [m["game"] for m in div["members"]] == ["Game C"]
|
||||||
assert div["vs_reference"]["axes"]["methods"] == {"added": 1, "removed": 0, "changed": 0}
|
assert div["vs_reference"]["axes"]["methods"] == {"added": 1, "removed": 0, "changed": 0}
|
||||||
assert 0 < div["vs_reference"]["overall"] < 100
|
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 A"]["diverges"] is False
|
||||||
assert lib["cells"]["Game C"]["diverges"] is True
|
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 "PIKLIB8.dll" in md
|
||||||
assert "\\*" in md # the divergence marker survives to the table
|
assert "\\*" in md # the divergence marker survives to the table
|
||||||
assert "vs **A**" in md # the footnote carries the delta
|
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():
|
def test_json_round_trips():
|
||||||
|
|||||||
Reference in New Issue
Block a user