*/ 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 softbenchResults(): HasMany { return $this->hasMany(SoftbenchResult::class); } public function infrabenchResults(): HasMany { return $this->hasMany(InfrabenchResult::class); } public function prosebenchResults(): HasMany { return $this->hasMany(ProsebenchResult::class); } public function psychbenchResults(): HasMany { return $this->hasMany(PsychbenchResult::class); } public function scenarioRuns(): HasMany { return $this->hasMany(ScenarioRun::class); } /** * The suite-specific result rows for this run, via the registry. Returns * an empty collection for suites this run does not have. */ public function suiteResults(): Collection { $model = Suites::modelFor($this->suite); if ($model === null) { return collect(); } $relation = Suites::runRelation($this->suite); return $this->{$relation}()->orderBy('task')->orderBy('id')->get(); } 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 { 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) { $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)); } }; } }