$contract */ $contract = Yaml::parseFile($contractPath); $declarations = $contract['x-derived-state-consumers'] ?? []; expect($declarations)->toBeArray()->not->toBeEmpty(); $allowedFamilies = [ 'artifact_truth', 'operation_ux_guidance', 'operation_ux_explanation', 'related_navigation_primary', 'related_navigation_detail', 'related_navigation_header', ]; $allowedAccessPatterns = ['row_safe', 'page_safe', 'direct_once']; $allowedFreshnessPolicies = ['request_stable', 'invalidate_after_mutation', 'no_reuse']; $cachePattern = '/static\s+array\s+\$[A-Za-z0-9_]*cache\b/i'; $violations = []; foreach ($declarations as $index => $declaration) { if (! is_array($declaration)) { $violations[] = [ 'surface' => 'contract', 'message' => sprintf('Declaration %d must be an object.', $index), ]; continue; } $surface = trim((string) ($declaration['surface'] ?? '')); $family = trim((string) ($declaration['family'] ?? '')); $variant = trim((string) ($declaration['variant'] ?? '')); $accessPattern = trim((string) ($declaration['accessPattern'] ?? '')); $freshnessPolicy = trim((string) ($declaration['freshnessPolicy'] ?? '')); $scopeInputs = $declaration['scopeInputs'] ?? null; $guardScope = $declaration['guardScope'] ?? null; $requiredMarkers = array_values(array_filter( $declaration['requiredMarkers'] ?? [], static fn (mixed $marker): bool => is_string($marker) && trim($marker) !== '', )); $maxOccurrences = $declaration['maxOccurrences'] ?? []; if ($surface === '' || $variant === '') { $violations[] = [ 'surface' => $surface !== '' ? $surface : 'contract', 'message' => 'Each declaration must provide non-empty surface and variant values.', ]; } if (! in_array($family, $allowedFamilies, true)) { $violations[] = [ 'surface' => $surface !== '' ? $surface : 'contract', 'message' => sprintf('Unsupported family "%s".', $family), ]; } if (! in_array($accessPattern, $allowedAccessPatterns, true)) { $violations[] = [ 'surface' => $surface !== '' ? $surface : 'contract', 'message' => sprintf('Unsupported accessPattern "%s".', $accessPattern), ]; } if (! in_array($freshnessPolicy, $allowedFreshnessPolicies, true)) { $violations[] = [ 'surface' => $surface !== '' ? $surface : 'contract', 'message' => sprintf('Unsupported freshnessPolicy "%s".', $freshnessPolicy), ]; } if (! is_array($scopeInputs) || $scopeInputs === []) { $violations[] = [ 'surface' => $surface !== '' ? $surface : 'contract', 'message' => 'Each declaration must include at least one scope input.', ]; } if (! is_array($guardScope) || $guardScope === []) { $violations[] = [ 'surface' => $surface !== '' ? $surface : 'contract', 'message' => 'Each declaration must include at least one guardScope path.', ]; continue; } foreach ($guardScope as $relativePath) { $relativePath = trim((string) $relativePath); $absolutePath = $root.'/'.$relativePath; if ($relativePath === '' || ! is_file($absolutePath)) { $violations[] = [ 'surface' => $surface !== '' ? $surface : 'contract', 'message' => sprintf('Missing guardScope file "%s".', $relativePath), ]; continue; } $source = SourceFileScanner::read($absolutePath); foreach ($requiredMarkers as $marker) { if (str_contains($source, $marker)) { continue; } $violations[] = [ 'surface' => $surface, 'file' => SourceFileScanner::relativePath($absolutePath), 'message' => sprintf('Missing required marker "%s".', $marker), ]; } if (preg_match($cachePattern, $source, $match, PREG_OFFSET_CAPTURE) === 1) { $offset = $match[0][1]; $line = substr_count(substr($source, 0, is_int($offset) ? $offset : 0), "\n") + 1; $violations[] = [ 'surface' => $surface, 'file' => SourceFileScanner::relativePath($absolutePath), 'line' => $line, 'message' => 'Ad hoc local cache detected in guarded surface.', 'snippet' => SourceFileScanner::snippet($source, $line), ]; } if (! is_array($maxOccurrences)) { continue; } foreach ($maxOccurrences as $occurrenceRule) { if (! is_array($occurrenceRule)) { continue; } $needle = trim((string) ($occurrenceRule['needle'] ?? '')); $max = (int) ($occurrenceRule['max'] ?? -1); if ($needle === '' || $max < 0) { continue; } $actual = substr_count($source, $needle); if ($actual <= $max) { continue; } $offset = strpos($source, $needle); $line = $offset === false ? 1 : substr_count(substr($source, 0, $offset), "\n") + 1; $violations[] = [ 'surface' => $surface, 'file' => SourceFileScanner::relativePath($absolutePath), 'line' => $line, 'message' => sprintf('Found %d occurrences of "%s"; expected at most %d.', $actual, $needle, $max), 'snippet' => SourceFileScanner::snippet($source, $line), ]; } } } if ($violations !== []) { $messages = array_map(static function (array $violation): string { $location = $violation['surface'] ?? 'contract'; if (isset($violation['file'])) { $location .= ' @ '.$violation['file']; } if (isset($violation['line'])) { $location .= ':'.$violation['line']; } $message = $location."\n".$violation['message']; if (isset($violation['snippet'])) { $message .= "\n".$violation['snippet']; } return $message; }, $violations); $this->fail( "Derived-state consumer guard violations found:\n\n".implode("\n\n", $messages) ); } expect($violations)->toBe([]); });