## Summary - move the Laravel application into `apps/platform` and keep the repository root for orchestration, docs, and tooling - update the local command model, Sail/Docker wiring, runtime paths, and ignore rules around the new platform location - add relocation quickstart/contracts plus focused smoke coverage for bootstrap, command model, routes, and runtime behavior ## Validation - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PlatformRelocation` - integrated browser smoke validated `/up`, `/`, `/admin`, `/admin/choose-workspace`, and tenant route semantics for `200`, `403`, and `404` ## Remaining Rollout Checks - validate Dokploy build context and working-directory assumptions against the new `apps/platform` layout - confirm web, queue, and scheduler processes all start from the expected working directory in staging/production - verify no legacy volume mounts or asset-publish paths still point at the old root-level `public/` or `storage/` locations Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #213
91 lines
2.8 KiB
PHP
91 lines
2.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Baselines;
|
|
|
|
final class BaselineSnapshotItemNormalizer
|
|
{
|
|
/**
|
|
* @param list<array{subject_type: string, subject_external_id: string, subject_key: string, policy_type: string, baseline_hash: string, meta_jsonb: array<string, mixed>}> $items
|
|
* @return array{items: list<array{subject_type: string, subject_external_id: string, subject_key: string, policy_type: string, baseline_hash: string, meta_jsonb: array<string, mixed>}>, duplicates: int}
|
|
*/
|
|
public function deduplicate(array $items): array
|
|
{
|
|
$uniqueItems = [];
|
|
$duplicates = 0;
|
|
|
|
foreach ($items as $item) {
|
|
$key = trim((string) ($item['subject_type'] ?? '')).'|'.trim((string) ($item['subject_external_id'] ?? ''));
|
|
|
|
if ($key === '|') {
|
|
continue;
|
|
}
|
|
|
|
if (! array_key_exists($key, $uniqueItems)) {
|
|
$uniqueItems[$key] = $item;
|
|
|
|
continue;
|
|
}
|
|
|
|
$duplicates++;
|
|
|
|
if ($this->shouldReplace($uniqueItems[$key], $item)) {
|
|
$uniqueItems[$key] = $item;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'items' => array_values($uniqueItems),
|
|
'duplicates' => $duplicates,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array{meta_jsonb?: array<string, mixed>, baseline_hash?: string} $current
|
|
* @param array{meta_jsonb?: array<string, mixed>, baseline_hash?: string} $candidate
|
|
*/
|
|
private function shouldReplace(array $current, array $candidate): bool
|
|
{
|
|
$currentFidelity = $this->fidelityRank($current);
|
|
$candidateFidelity = $this->fidelityRank($candidate);
|
|
|
|
if ($candidateFidelity !== $currentFidelity) {
|
|
return $candidateFidelity > $currentFidelity;
|
|
}
|
|
|
|
$currentObservedAt = $this->observedAt($current);
|
|
$candidateObservedAt = $this->observedAt($candidate);
|
|
|
|
if ($candidateObservedAt !== $currentObservedAt) {
|
|
return $candidateObservedAt > $currentObservedAt;
|
|
}
|
|
|
|
return strcmp((string) ($candidate['baseline_hash'] ?? ''), (string) ($current['baseline_hash'] ?? '')) > 0;
|
|
}
|
|
|
|
/**
|
|
* @param array{meta_jsonb?: array<string, mixed>} $item
|
|
*/
|
|
private function fidelityRank(array $item): int
|
|
{
|
|
$fidelity = data_get($item, 'meta_jsonb.evidence.fidelity');
|
|
|
|
return match ($fidelity) {
|
|
'content' => 2,
|
|
'meta' => 1,
|
|
default => 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array{meta_jsonb?: array<string, mixed>} $item
|
|
*/
|
|
private function observedAt(array $item): string
|
|
{
|
|
$observedAt = data_get($item, 'meta_jsonb.evidence.observed_at');
|
|
|
|
return is_string($observedAt) ? $observedAt : '';
|
|
}
|
|
}
|