From 2c785dc87cdbbdc89b430d4c02ca7d9b1aa00dc0 Mon Sep 17 00:00:00 2001 From: Patryk Gensch <43010113+patryk025@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:32:31 +0200 Subject: [PATCH] feat: add compatibility table endpoint and rendering for game libraries --- .gitignore | 1 + README.md | 32 +++- ams/api/app.py | 3 +- ams/api/routes/compat.py | 49 ++++++ ams/api/static/index.html | 6 +- ams/api/static/style.css | 3 +- ams/compat.py | 336 ++++++++++++++++++++++++++++++++++++++ tests/test_compat.py | 149 +++++++++++++++++ 8 files changed, 575 insertions(+), 4 deletions(-) create mode 100644 ams/api/routes/compat.py create mode 100644 ams/compat.py create mode 100644 tests/test_compat.py diff --git a/.gitignore b/.gitignore index 62d9dcb..71b06d3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ __pycache__/ *.pyc .venv/ +*.egg-info/ # macOS .DS_Store diff --git a/README.md b/README.md index 016cc6d..c0a8300 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,8 @@ python -m ams.api.importer --game "Reksio i UFO" snapshots/PIKLIB8.dll.snapshot. uvicorn ams.api.app:create_app --factory --reload # serwer ``` Endpointy: `POST/GET /games`, `POST/GET /snapshots` (import deduplikowany po sha256), -`GET /diff?old=&new=[&owner=]`, `GET /snapshots/{id}/similar`, `POST/GET /jobs`, `GET /health`. +`GET /diff?old=&new=[&owner=]`, `GET /snapshots/{id}/similar`, `GET /compat?format=md|json`, +`POST/GET /jobs`, `GET /health`. Testy: `pytest` (28, w tym integracyjne na golden pair). ### Podobne wersje @@ -211,6 +212,35 @@ Miara jest *cross-compiler*: golden pair PIKLIB8 (MSVC6) ↔ bloomoodll (MSVC8) (types 95% / methods 87% / events 77% / fields 90%), tam gdzie fuzzy-hash binarki daje 0. Fuzzy (ssdeep) leci jako sygnał poboczny „prawie ten sam plik", gdy snapshot ma `binary.fuzzy`. +## Tablica kompatybilności (eksport pod dokumentację) + +Krosowy widok całego katalogu **grupowany po nazwie biblioteki** (`PIKLIB8.dll`, `bloomoodll.dll`…): +która gra wiezie którą bibliotekę. Haczyk: ta sama nazwa może wystąpić w kilku grach, a *mimo to* +nie być tym samym silnikiem. Dlatego w obrębie jednej nazwy klastrujemy **warianty po powierzchni +silnika** (te same zbiory tożsamości co `diff`/`similarity` — kompilator-agnostyczne), a każdy +wariant inny niż referencyjny (`A`) dostaje **gwiazdkę** `*` z deltą *czym* się różni. Byte-różnice +z rekompilacji nie generują gwiazdki — liczy się API skryptowe. + +```bash +# z plików snapshot (bez DB/serwera); Label= ustawia kolumnę-grę +python -m ams.compat \ + "Reksio i UFO=snapshots/PIKLIB8.dll.snapshot.json" \ + "Reksio i Kapitan Nemo=snapshots/bloomoodll.dll.snapshot.json" \ + --format md -o compat.md # albo --format json +``` + +Z katalogu (DB) przez API — pobiera gotowy plik (`Content-Disposition: attachment`): + +```bash +curl -OJ "http://127.0.0.1:8000/compat?format=md" # → compat.md +curl -OJ "http://127.0.0.1:8000/compat?format=json" # → compat.json (grupy + warianty + diffy) +``` + +W UI: przycisk **⬇ kompat** 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. + ## Front — Command Center Po starcie serwera otwórz **http://127.0.0.1:8000/** (`/` → `/ui/`). Statyczny UI bez build-stepu diff --git a/ams/api/app.py b/ams/api/app.py index 0f9a601..a70fec6 100644 --- a/ams/api/app.py +++ b/ams/api/app.py @@ -15,7 +15,7 @@ from fastapi.staticfiles import StaticFiles from .. import __version__ from .db import configure, init_db -from .routes import diff, games, jobs, snapshots +from .routes import compat, diff, games, jobs, snapshots _STATIC = Path(__file__).parent / "static" @@ -29,6 +29,7 @@ def create_app(database_url: str | None = None) -> FastAPI: app.include_router(snapshots.router) app.include_router(diff.router) app.include_router(jobs.router) + app.include_router(compat.router) @app.get("/health", tags=["meta"]) def health() -> dict[str, str]: diff --git a/ams/api/routes/compat.py b/ams/api/routes/compat.py new file mode 100644 index 0000000..6e7de2e --- /dev/null +++ b/ams/api/routes/compat.py @@ -0,0 +1,49 @@ +"""Catalog-wide compatibility table, grouped by library name, as a downloadable file. + + GET /compat?format=md -> Markdown table (attachment compat.md) + GET /compat?format=json -> structured JSON (attachment compat.json) + +Builds `ams.compat` entries from every catalogued snapshot, labelling each by its game (or +'(bez gry)' when unlinked). Same engine library name across games clusters into surface variants, +so divergent copies get an asterisk — exactly what the docs table needs. +""" + +from __future__ import annotations + +from fastapi import APIRouter, Depends, Query +from fastapi.responses import Response +from sqlalchemy import select +from sqlalchemy.orm import Session + +from ...compat import Entry, compatibility_table, render_json, render_markdown +from ...snapshot import Snapshot as Surface +from .. import models +from ..db import get_db + +router = APIRouter(tags=["compat"]) + +_NO_GAME = "(bez gry)" + + +@router.get("/compat") +def get_compat( + format: str = Query("md", pattern="^(md|json)$", description="md = Markdown, json = structured"), + db: Session = Depends(get_db), +) -> Response: + entries = [ + Entry(game=snap.game.name if snap.game else _NO_GAME, snapshot=Surface(snap.data)) + for snap in db.scalars(select(models.Snapshot).order_by(models.Snapshot.id)) + ] + table = compatibility_table(entries) + + if format == "json": + return Response( + content=render_json(table), + media_type="application/json", + headers={"Content-Disposition": 'attachment; filename="compat.json"'}, + ) + return Response( + content=render_markdown(table), + media_type="text/markdown; charset=utf-8", + headers={"Content-Disposition": 'attachment; filename="compat.md"'}, + ) diff --git a/ams/api/static/index.html b/ams/api/static/index.html index 1fc2e39..05b13c7 100644 --- a/ams/api/static/index.html +++ b/ams/api/static/index.html @@ -16,7 +16,11 @@