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>
59 lines
2.1 KiB
Python
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
|
|
]
|