Files
Patryk Gensch 38be932abc Similar versions: surface-overlap metric + endpoint + UI panel
Ranks catalogued engine versions by how much of their CMC_* surface they share,
which (unlike a binary fuzzy hash) stays meaningful across compilers — the golden
pair PIKLIB8/MSVC6 vs bloomoodll/MSVC8 scores 85%.

- similarity.py: jaccard, surface_similarity (per-axis + pooled overall),
  fuzzy_similarity (ssdeep via ppdeep, secondary signal)
- service.similar_snapshots + GET /snapshots/{id}/similar?min=N (SimilarHit)
- UI: "Podobne wersje" panel in the snapshot browser (overlap bar + ⇄ diff)
- tests: 6 new (jaccard, identical/disjoint, golden pair 0<x<100, fuzzy,
  endpoint + min filter) -> 28/28

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:33:50 +02:00

59 lines
2.1 KiB
Python

from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException, Query
from sqlalchemy import select
from sqlalchemy.orm import Session
from .. import models, schemas, service
from ..db import get_db
router = APIRouter(prefix="/snapshots", tags=["snapshots"])
@router.post("", response_model=schemas.SnapshotOut, status_code=201)
def create_snapshot(
data: dict[str, Any] = Body(..., description="a full engine-surface snapshot.json"),
game: str | None = Query(None, description="link to / create this game by name"),
db: Session = Depends(get_db),
) -> models.Snapshot:
if not service.looks_like_snapshot(data):
raise HTTPException(422, "body is not an engine-surface snapshot (missing binary/types)")
return service.import_snapshot(db, data, game)
@router.get("", response_model=list[schemas.SnapshotOut])
def list_snapshots(
game_id: int | None = Query(None), db: Session = Depends(get_db)
) -> list[models.Snapshot]:
q = select(models.Snapshot)
if game_id is not None:
q = q.where(models.Snapshot.game_id == game_id)
return list(db.scalars(q.order_by(models.Snapshot.id)))
@router.get("/{snapshot_id}", response_model=schemas.SnapshotDetail)
def get_snapshot(snapshot_id: int, db: Session = Depends(get_db)) -> models.Snapshot:
snap = db.get(models.Snapshot, snapshot_id)
if snap is None:
raise HTTPException(404, "snapshot not found")
return snap
@router.get("/{snapshot_id}/similar", response_model=list[schemas.SimilarHit])
def similar_snapshots(
snapshot_id: int,
min: int = Query(0, ge=0, le=100, description="drop hits below this overall score"),
db: Session = Depends(get_db),
) -> list[schemas.SimilarHit]:
hits = service.similar_snapshots(db, snapshot_id, minimum=min)
if hits is None:
raise HTTPException(404, "snapshot not found")
return [
schemas.SimilarHit(
snapshot=schemas.SnapshotOut.model_validate(snap),
overall=score["overall"], fuzzy=score["fuzzy"], axes=score["axes"])
for snap, score in hits
]