2026-09-17 02:34:43 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models;
|
|
|
|
|
|
|
|
|
|
use Database\Factories\BenchmarkRunFactory;
|
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
|
|
|
|
|
|
|
class BenchmarkRun extends Model
|
|
|
|
|
{
|
|
|
|
|
/** @use HasFactory<BenchmarkRunFactory> */
|
|
|
|
|
use HasFactory;
|
|
|
|
|
|
|
|
|
|
public const VISIBILITY_PUBLIC = 'public';
|
|
|
|
|
|
|
|
|
|
public const VISIBILITY_PRIVATE = 'private';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Columns written only at submission/import time. Everything here is
|
|
|
|
|
* immutable once stored; user updates may only touch $overlay.
|
|
|
|
|
*/
|
|
|
|
|
protected $fillable = [
|
|
|
|
|
'run_id', 'ran_at', 'suite', 'run_key',
|
|
|
|
|
'model_id', 'model_quant', 'model_quant_raw', 'model_path',
|
|
|
|
|
'engine', 'engine_build', 'n_ctx', 'slots',
|
|
|
|
|
'gpu', 'gpu_count', 'gpu_source', 'host', 'client', 'endpoint',
|
|
|
|
|
'label', 'config', 'image_digest', 'validators', 'suite_versions',
|
|
|
|
|
'harness', 'harness_ver', 'harness_mode', 'user_id',
|
|
|
|
|
'visibility', 'notes',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** The only fields a user may change after submission. */
|
|
|
|
|
public const OVERLAY_FIELDS = ['visibility', 'notes'];
|
|
|
|
|
|
|
|
|
|
protected function casts(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'ran_at' => 'datetime',
|
|
|
|
|
'validators' => 'array',
|
|
|
|
|
'suite_versions' => 'array',
|
|
|
|
|
'n_ctx' => 'integer',
|
|
|
|
|
'slots' => 'integer',
|
|
|
|
|
'gpu_count' => 'integer',
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Submitted metrics are immutable: any update that touches a column
|
|
|
|
|
* outside OVERLAY_FIELDS is silently reverted before it hits the
|
|
|
|
|
* database. This is the safety net for every user-facing update path.
|
|
|
|
|
*/
|
|
|
|
|
protected static function booted(): void
|
|
|
|
|
{
|
|
|
|
|
static::updating(function (self $run): void {
|
|
|
|
|
foreach ($run->getDirty() as $key => $_) {
|
|
|
|
|
if (! in_array($key, self::OVERLAY_FIELDS, true)) {
|
|
|
|
|
$run->setAttribute($key, $run->getOriginal($key));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function user(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(User::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function results(): HasMany
|
|
|
|
|
{
|
|
|
|
|
return $this->hasMany(RunResult::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isOwnedBy(?User $user): bool
|
|
|
|
|
{
|
|
|
|
|
return $user !== null && $this->user_id === $user->id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Runs visible to $user: everything public, plus the user's own private
|
|
|
|
|
* runs. Applied as a local scope so every listing query starts from it.
|
|
|
|
|
*/
|
|
|
|
|
public function scopeVisibleTo(Builder $query, ?User $user): Builder
|
|
|
|
|
{
|
2026-09-17 02:41:12 +00:00
|
|
|
return $query->where(self::visibleClause($user));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The visibility predicate as a standalone closure, so joined queries
|
|
|
|
|
* (e.g. the flat results table) can apply the exact same rule without
|
|
|
|
|
* duplicating the logic.
|
|
|
|
|
*
|
|
|
|
|
* @return \Closure(Builder): Builder
|
|
|
|
|
*/
|
|
|
|
|
public static function visibleClause(?User $user): \Closure
|
|
|
|
|
{
|
|
|
|
|
return function (Builder $q) use ($user) {
|
2026-09-17 02:34:43 +00:00
|
|
|
$q->where('visibility', self::VISIBILITY_PUBLIC);
|
|
|
|
|
|
|
|
|
|
if ($user !== null) {
|
|
|
|
|
$q->orWhere(fn (Builder $own) => $own
|
|
|
|
|
->where('visibility', self::VISIBILITY_PRIVATE)
|
|
|
|
|
->where('user_id', $user->id));
|
|
|
|
|
}
|
2026-09-17 02:41:12 +00:00
|
|
|
};
|
2026-09-17 02:34:43 +00:00
|
|
|
}
|
|
|
|
|
}
|