>,links:list>} */ public function plan(): array { $file = base_path('docs/investmoz/unification/approved-anchor-territories-20261023.json'); $contents = is_file($file) ? file_get_contents($file) : false; if ($contents !== false) { $contents = str_replace("\r\n", "\n", $contents); } if ($contents === false || ! hash_equals(self::HASH, hash('sha256', $contents))) { throw new RuntimeException('Reviewed territorial manifest changed.'); } $manifest = json_decode($contents, true, 512, JSON_THROW_ON_ERROR); if (count($manifest['items']) !== 28) { throw new RuntimeException('Exact 28-link scope required.'); } $projects = []; $links = []; foreach ($manifest['items'] as $item) { $project = DB::table('projects')->where('id', $item['project_id'])->whereNull('deleted_at')->first(); $dossier = DB::table('investment_dossiers')->where('id', $item['dossier_id'])->whereNull('deleted_at')->first(); if (! $project || $project->code !== $item['code'] || ! $dossier || $dossier->project_id !== $project->id || ! hash_equals($item['content_sha256'], hash('sha256', $dossier->content)) || ! hash_equals($item['source_sha256'], hash('sha256', $dossier->source))) { throw new RuntimeException('Project identity or documentary source changed: '.$item['code']); } $original = $item['original_relation']; $geo = DB::table('geo_locations')->where('id', $original['geo_location_id'])->whereNull('deleted_at')->first(); foreach (['location_type', 'source_code', 'parent_id', 'data_classification'] as $field) { if (! $geo || $item['territory'][$field] !== $geo->$field) { throw new RuntimeException('Territory identity changed: '.$original['geo_location_id']); } } $metadata = $projects[$project->id]['after_metadata'] ?? json_decode($project->metadata, true, 512, JSON_THROW_ON_ERROR); $key = $geo->id.':'.$original['role']; $evidence = $item['evidence']; // Preserve the original dossier id in evidence and explicitly map the destination. $evidence['transported_dossier_id'] = $dossier->id; if (isset($metadata['documented_location_sources'][$key]) && $metadata['documented_location_sources'][$key] != $evidence) { throw new RuntimeException('Existing territorial evidence differs; no overwrite: '.$item['code']); } $projects[$project->id] ??= ['id' => $project->id, 'code' => $project->code, 'before_version' => (int) $project->version_no, 'before_metadata' => json_decode($project->metadata, true, 512, JSON_THROW_ON_ERROR)]; $metadata['documented_location_sources'][$key] = $evidence; $projects[$project->id]['after_metadata'] = $metadata; $existing = DB::table('project_locations')->where('project_id', $project->id)->where('geo_location_id', $geo->id)->where('role', $original['role'])->first(); if ($existing && ($existing->deleted_at || $existing->data_classification !== 'public')) { throw new RuntimeException('Archived or restricted territorial relation must not be revived.'); } $identity = DB::table('project_locations')->where('id', $original['id'])->first(); if ($identity && (! $existing || $identity->id !== $existing->id)) { throw new RuntimeException('Historical relation identity is already occupied.'); } $links[] = ['code' => $item['code'], 'already_present' => (bool) $existing, 'row' => [...$original, 'project_id' => $project->id]]; } return ['manifest_sha256' => self::HASH, 'projects' => array_values($projects), 'links' => $links]; } /** Caller supplies an authenticated, backed-up transaction; no source publication changes. * @param array{manifest_sha256:string,projects:list>,links:list>} $plan * @return array */ public function apply(array $plan): array { if (DB::transactionLevel() < 1 || $this->plan() !== $plan) { throw new RuntimeException('Reviewed transaction and unchanged preview required.'); } $inserted = 0; $updated = 0; foreach ($plan['projects'] as $project) { if ($project['before_metadata'] == $project['after_metadata']) { continue; } $count = DB::table('projects')->where('id', $project['id'])->where('version_no', $project['before_version'])->update([ 'metadata' => json_encode($project['after_metadata'], JSON_THROW_ON_ERROR), 'version_no' => $project['before_version'] + 1, 'updated_at' => now(), ]); if ($count !== 1) { throw new RuntimeException('Concurrent project change.'); } DB::table('audit_events')->insert(['source_system' => 'approved-anchor-territories-recovery-v1', 'external_id' => self::HASH.':'.$project['id'], 'entity_type' => 'projects', 'entity_id' => $project['id'], 'action' => 'accepted_territories_restored', 'data_classification' => 'public', 'before_data' => json_encode(['version_no' => $project['before_version'], 'metadata' => $project['before_metadata']], JSON_THROW_ON_ERROR), 'after_data' => json_encode(['version_no' => $project['before_version'] + 1, 'manifest_sha256' => self::HASH, 'metadata' => $project['after_metadata'], 'new_approval' => false], JSON_THROW_ON_ERROR)]); $updated++; } foreach ($plan['links'] as $link) { if ($link['already_present']) { continue; } DB::table('project_locations')->insert($link['row']); $inserted++; } return ['relations_restored' => $inserted, 'projects_updated' => $updated, 'new_dossiers' => 0, 'publication_changes' => 0]; } }